重建历史 table 行,仅基于更改日志数据

Reconstruct historical table rows, based only on change-log data

我正在研究一些复杂的销售分析,这非常复杂……而且很无聊……

所以对于这个问题,我将使用一个有趣的含糖比喻:[=​​93=]自动售货机。
但我实际的 table 结构也是一样的。
(你可以假设有很多索引、约束等)

假设我们有一个 table 包含自动售货机库存数据。
table 简单地显示了每台自动售货机当前可用的每种糖果的数量。

我知道,通常会有一个 ITEM_TYPE table 包含 'Snickers'[= 的行150=] 等等,但由于多种原因,我们的 table 并不是这样构造的。
实际上,它不是产品数量,而是预先汇总的销售数据:"Pipeline Total""Forecast Total"、等等
因此,我必须使用一个简单的 table,其中包含用于不同 "types" 总计的单独列。

对于这个例子,我还添加了一些文本列,以证明我必须考虑多种数据类型。
(这使事情变得复杂。)

除了 ID,所有列都可以为空 - 这是一个真正的问题。
就我们而言,如果该列是 NULL,那么 NULL 就是我们需要用于分析和报告的官方值。

CREATE table "VENDING_MACHINES" (
    "ID"                 NUMBER NOT NULL ENABLE,
    "SNICKERS_COUNT"     NUMBER,
    "MILKY_WAY_COUNT"    NUMBER,
    "TWIX_COUNT"         NUMBER,
    "SKITTLES_COUNT"     NUMBER,
    "STARBURST_COUNT"    NUMBER,
    "SWEDISH_FISH_COUNT" NUMBER,
    "FACILITIES_ADDRESS" VARCHAR2(100),
    "FACILITIES_CONTACT" VARCHAR2(100),

    CONSTRAINT "VENDING_MACHINES_PK" PRIMARY KEY ("ID") USING INDEX ENABLE
)
/

示例数据:

INSERT INTO VENDING_MACHINES (ID, SNICKERS_COUNT, MILKY_WAY_COUNT, TWIX_COUNT,
                              SKITTLES_COUNT, STARBURST_COUNT, SWEDISH_FISH_COUNT,
                              FACILITIES_ADDRESS, FACILITIES_CONTACT)
SELECT 225, 11, 15, 14, 0, NULL, 13, '123 Abc Street', 'Steve' FROM DUAL UNION ALL
SELECT 349, NULL, 7, 3, 11, 8, 7, NULL, '' FROM DUAL UNION ALL
SELECT 481, 8, 4, 0, NULL, 14, 3, '1920 Tenaytee Way', NULL FROM DUAL UNION ALL
SELECT 576, 4, 2, 8, 4, 9, NULL, '', 'Angela' FROM DUAL

自动售货机将定期连接到数据库并更新其库存记录。
也许他们每次有人买东西时更新,或者他们每 30 分钟更新一次,或者他们可能只在有人重新装满糖果时更新 - 老实说,这并不重要。

重要的是,每当 VENDING_MACHINES table 中的记录更新时,都会执行一个触发器,将每个单独的更改记录在单独的日志中 table VENDING_MACHINES_CHANGE_LOG.
这个触发器已经写好了,效果很好
(如果列 "updated" 具有与已经存在的相同值,则更改 应该 被触发器忽略。)

VENDING_MACHINES table 中修改的每个 记录一个单独的行(ID 除外)。
因此,如果在 VENDING_MACHINES table 中插入一个全新的行(即一台新的自动售货机),将在 VENDING_MACHINES_CHANGE_LOG table 中记录八行 - 每个VENDING_MACHINES.

中的非 ID 列

(在我的真实场景中,有 90 多个列被跟踪。
但通常在任何给定时间只更新一两列,因此不会失控。)

此 "change log" 旨在成为 VENDING_MACHINES table 的永久历史记录,因此我们不会创建外键约束 - 如果从 [= 中删除了一行20=] 我们想在更改日志中保留孤立的历史记录。
此外,Apex 不支持 ON UPDATE CASCADE (?),因此触发器必须检查 ID 列的更新,并在整个相关 table 中手动传播更新(例如更改日志)。

CREATE table "VENDING_MACHINE_CHANGE_LOG" (
    "ID"                   NUMBER       NOT NULL ENABLE,
    "CHANGE_TIMESTAMP"     TIMESTAMP(6) NOT NULL ENABLE,
    "VENDING_MACHINE_ID"   NUMBER       NOT NULL ENABLE,
    "MODIFIED_COLUMN_NAME" VARCHAR2(30) NOT NULL ENABLE,

    "MODIFIED_COLUMN_TYPE" VARCHAR2(30) GENERATED ALWAYS AS
        (CASE "MODIFIED_COLUMN_NAME" WHEN 'FACILITIES_ADDRESS' THEN 'TEXT'
                                     WHEN 'FACILITIES_CONTACT' THEN 'TEXT'
                                     ELSE 'NUMBER' END) VIRTUAL NOT NULL ENABLE,

    "NEW_NUMBER_VALUE"     NUMBER,
    "NEW_TEXT_VALUE"       VARCHAR2(4000),

    CONSTRAINT "VENDING_MACHINE_CHANGE_LOG_CK" CHECK
        ("MODIFIED_COLUMN_NAME" IN('SNICKERS_COUNT', 'MILKY_WAY_COUNT', 'TWIX_COUNT',
                                   'SKITTLES_COUNT', 'STARBURST_COUNT', 'SWEDISH_FISH_COUNT',
                                   'FACILITIES_ADDRESS', 'FACILITIES_CONTACT')) ENABLE,

    CONSTRAINT "VENDING_MACHINE_CHANGE_LOG_PK" PRIMARY KEY ("ID") USING INDEX ENABLE,

    CONSTRAINT "VENDING_MACHINE_CHANGE_LOG_UK" UNIQUE ("CHANGE_TIMESTAMP",
                                                       "VENDING_MACHINE_ID",
                                                       "MODIFIED_COLUMN_NAME") USING INDEX ENABLE

    /* No foreign key, since we want this change log to be orphaned and preserved.
       Also, apparently Apex doesn't support ON UPDATE CASCADE for some reason? */
)
/

更改日志示例数据:

INSERT INTO VENDING_MACHINE_CHANGE_LOG (ID, CHANGE_TIMESTAMP, VENDING_MACHINE_ID,
                                        MODIFIED_COLUMN_NAME, NEW_NUMBER_VALUE, NEW_TEXT_VALUE)
SELECT 167, '11/06/19 05:18', 481, 'MILKY_WAY_COUNT', 5, NULL FROM DUAL UNION ALL
SELECT 168, '11/06/19 05:21', 225, 'SWEDISH_FISH_COUNT', 1, NULL FROM DUAL UNION ALL
SELECT 169, '11/06/19 05:40', 481, 'FACILITIES_ADDRESS', NULL, NULL FROM DUAL UNION ALL
SELECT 170, '11/06/19 05:49', 481, 'STARBURST_COUNT', 4, NULL FROM DUAL UNION ALL
SELECT 171, '11/06/19 06:09', 576, 'FACILITIES_CONTACT', NULL, '' FROM DUAL UNION ALL
SELECT 172, '11/06/19 06:25', 481, 'SWEDISH_FISH_COUNT', 7, NULL FROM DUAL UNION ALL
SELECT 173, '11/06/19 06:40', 481, 'FACILITIES_CONTACT', NULL, 'Audrey' FROM DUAL UNION ALL
SELECT 174, '11/06/19 06:46', 576, 'SNICKERS_COUNT', 13, NULL FROM DUAL UNION ALL
SELECT 175, '11/06/19 06:55', 576, 'FACILITIES_ADDRESS', NULL, '388 Holiday Street' FROM DUAL UNION ALL
SELECT 176, '11/06/19 06:59', 576, 'SWEDISH_FISH_COUNT', NULL, NULL FROM DUAL UNION ALL
SELECT 177, '11/06/19 07:00', 349, 'MILKY_WAY_COUNT', 3, NULL FROM DUAL UNION ALL
SELECT 178, '11/06/19 07:03', 481, 'TWIX_COUNT', 8, NULL FROM DUAL UNION ALL
SELECT 179, '11/06/19 07:11', 349, 'TWIX_COUNT', 15, NULL FROM DUAL UNION ALL
SELECT 180, '11/06/19 07:31', 225, 'FACILITIES_CONTACT', NULL, 'William' FROM DUAL UNION ALL
SELECT 181, '11/06/19 07:49', 576, 'FACILITIES_CONTACT', NULL, 'Brian' FROM DUAL UNION ALL
SELECT 182, '11/06/19 08:28', 481, 'SNICKERS_COUNT', 0, NULL FROM DUAL UNION ALL
SELECT 183, '11/06/19 08:38', 481, 'SKITTLES_COUNT', 7, '' FROM DUAL UNION ALL
SELECT 184, '11/06/19 09:04', 349, 'MILKY_WAY_COUNT', 10, NULL FROM DUAL UNION ALL
SELECT 185, '11/06/19 09:21', 481, 'SNICKERS_COUNT', NULL, NULL FROM DUAL UNION ALL
SELECT 186, '11/06/19 09:33', 225, 'SKITTLES_COUNT', 11, NULL FROM DUAL UNION ALL
SELECT 187, '11/06/19 09:45', 225, 'FACILITIES_CONTACT', NULL, NULL FROM DUAL UNION ALL
SELECT 188, '11/06/19 10:16', 481, 'FACILITIES_CONTACT', 4, 'Lucy' FROM DUAL UNION ALL
SELECT 189, '11/06/19 10:25', 481, 'SNICKERS_COUNT', 10, NULL FROM DUAL UNION ALL
SELECT 190, '11/06/19 10:57', 576, 'SWEDISH_FISH_COUNT', 12, NULL FROM DUAL UNION ALL
SELECT 191, '11/06/19 10:59', 225, 'MILKY_WAY_COUNT', NULL, NULL FROM DUAL UNION ALL
SELECT 192, '11/06/19 11:11', 481, 'STARBURST_COUNT', 6, 'Stanley' FROM DUAL UNION ALL
SELECT 193, '11/06/19 11:34', 225, 'SKITTLES_COUNT', 8, NULL FROM DUAL UNION ALL
SELECT 194, '11/06/19 11:39', 349, 'FACILITIES_CONTACT', NULL, 'Mark' FROM DUAL UNION ALL
SELECT 195, '11/06/19 11:42', 576, 'SKITTLES_COUNT', 8, NULL FROM DUAL UNION ALL
SELECT 196, '11/06/19 11:56', 225, 'TWIX_COUNT', 2, NULL FROM DUAL

我需要构建一个视图来重建完整的历史 VENDING_MACHINES table,仅使用来自 VENDING_MACHINE_CHANGE_LOG table.
的数据 即,由于允许孤立更改日志行,因此之前从 VENDING_MACHINES 中删除的行应该重新出现。
生成的视图应该允许我检索任何 VENDING_MACHINE 行,就像它在历史上任何特定点存在的那样。

VENDING_MACHINE_CHANGE_LOG 的示例数据非常短,不足以产生完整的结果...
但这应该足以证明预期的结果。

最终我认为需要分析函数。
但我是 SQL 分析函数的新手,也是 Oracle 和 Apex 的新手。
所以我不确定如何解决这个问题 - 重建原始 table 行的最佳方法是什么?

这是期望的结果(按 CHANGE_TIMESTAMP 排序):

这是相同的期望结果,另外按 VENDING_MACHINE_ID:

排序

我已经构建了一个简单的查询来为每个 VENDING_MACHINE_ID 提取最新的列值,但我认为这种方法不适合table 这个庞大的任务。
我想我需要改用分析函数,以获得更好的性能和灵活性。 (也许我错了?)

select vmcl.ID,
       vmcl.CHANGE_TIMESTAMP,
       vmcl.VENDING_MACHINE_ID,
       vmcl.MODIFIED_COLUMN_NAME,
       vmcl.MODIFIED_COLUMN_TYPE,
       vmcl.NEW_NUMBER_VALUE,
       vmcl.NEW_TEXT_VALUE

from ( select sqvmcl.VENDING_MACHINE_ID,
              sqvmcl.MODIFIED_COLUMN_NAME,
              max(sqvmcl.CHANGE_TIMESTAMP) as LAST_CHANGE_TIMESTAMP
       from VENDING_MACHINE_CHANGE_LOG sqvmcl
       where sqvmcl.CHANGE_TIMESTAMP <= /*[Current timestamp, or specified timestamp]*/
       group by sqvmcl.VENDING_MACHINE_ID, sqvmcl.MODIFIED_COLUMN_NAME ) sq

left join VENDING_MACHINE_CHANGE_LOG vmcl on vmcl.VENDING_MACHINE_ID = sq.VENDING_MACHINE_ID
                                         and vmcl.MODIFIED_COLUMN_NAME = sq.MODIFIED_COLUMN_NAME
                                         and vmcl.CHANGE_TIMESTAMP = sq.LAST_CHANGE_TIMESTAMP

请注意 left join 专门命中 VENDING_MACHINE_CHANGE_LOG table 的唯一索引 - 这是设计使然。

虽然这当然可以做到,但使用标准 SQL 无法有效地做到这一点,因为您的 table 违反了关系数据库的基本规则。具体来说,一个 table 中的一列被另一个中其名称的文本表示所引用。这是关系元数据中未捕获的关键关系。

因此,鉴于无法高效完成,问题是您可以容忍多大程度的低效率以及您想要做出哪些权衡?通常写这种变更日志是因为考虑到完整的历史 table 太大了,但也经常是很久以前就做出了这个决定,而现在我们坚定地"big data" 曾经是 "too big" 的时代现在是 "no problem"。

  • 如果您可以接受生成的 table 的大小,我希望创建一个包含您感兴趣的信息的完全具体化的 table。
  • 如果fulltable太大了,能不能通过减小时间戳的粒度,让它变成suitable大小?每天或每周一行,而不是每次更新一行。
  • 或者,您可以通过限制时间范围来减小大小,比如只限制过去 90 天吗?

如果这些选项中的任何一个是 suitable,那么您可以 运行 定期(例如每晚)的数据仓库过程(本质上是 ETL 过程)来创建和更新 table。请注意,如果您不需要对其进行连接,则生成的 table 不需要位于同一数据库中。您还可以修改触发器或创建新触发器,以在手动创建扩展后 table 使其保持最新。

否则将很难动态地执行此操作,因为您必须在 SQL 中明确说明每列的 valuescolumns 的映射。

我的建议实际上是完全更改 LOG table,而不是只记录您更改的列以及您在那里更改的内容。每次更新一行时,您将旧行插入到 LOG table 中,连同用于插入、更新、删除的标记、时间戳和 log_id.

然后当你想知道 table 在某个时间的状态时,你只需做一个查询(或构建一个简单的视图来进一步简化这个)并且你 select max所需日期之前的时间戳,用于不同的自动售货机。基本上,select 该自动售货机的最新日志条目,仍然在您想要的日期之前(如果最近的条目被删除,则不显示它)。

这种做事方式会大大简化事情,它会占用更多 space(但现在 space 很便宜),并且您的更新触发器可能会带来轻微的性能提升. 更不用说这也可以完美地处理插入和删除的行问题。 但是你对这个 table 的看法应该非常快,我敢打赌它会比你用这个当前日志 table.

拼凑的任何东西快得多

不过,如果您必须使用当前日志 table,我不确定 VIEW 是否会削减它。我想你需要做的是制作另一个与你现有的 VENDING_MACHINES table 相同的临时 table,然后当你输入你想要数据的日期时 运行 一些PLSQL.

然后我们遇到了一个问题,因为您的 LOG table 正在记录新值而不是旧值。

所以我要做的是 运行 一个 PLSQL 过程,select 在你想要的日期之后所有不同的变化(如果一台机器更新士力架计数 13 次,只需要一个的那些),以便找到自您想要的日期以来已更改的所有内容。然后找到该列在您想要的日期之前最后一次更新或插入的时间,并从那里获取值。这将需要一些动态的 SQL 魔法,并且编码和 运行.

会很痛苦

所以如果你不能做我建议的整个table改变,但仍然可以改变触发器,将OLD值插入LOG table,新记录存储在VENDING_MACHINES table 无论如何。在这种情况下,您可能仍需要创建 VENDING_MACHINES table 的副本,但这次 PLSQL 过程会简单得多,因为您只需在日期,从最近到最旧,对于每次更改,您都执行一个简单的动态 SQL 来反转它。

我强烈建议您使用第一种方法来改变您的 LOG table 的形成方式。因为这将更简单、更容易实施,并且更快 运行.

编辑:想出另一种解决问题的方法。首先,您将设置一个视图来更改 LOG table 的显示方式,使其与 VENDING_MACHINES table 具有相同的形式,具有相同的列,..这将是非常简单,看起来像这样:

SELECT change_id, change_timestamp, vending_machine_id,
       CASE WHEN modified_column_name = 'SNICKERS' THEN new_number_value ELSE NULL END AS snickers, 
      CASE WHEN modified_column_name = 'MILKY_WAY' THEN new_number_value ELSE NULL END AS 'milky_way',
.....


    CASE WHEN modified_column_name = 'FACILITIES_ADDRESS' then new_text_value ELSE NULL END AS 'facilities address'
  FROM log

然后您在此之上设置另一个视图,它实际上让您获得了您想要的日期。新视图的结构与原始 VENDING_MACHINES table 相似,具有不同的 vending_machine_ids,但对于每一列,您 select 该列中的值来自时间戳为最近并且该值不为空(select 对该列的最新更改),您将需要以某种方式找出一种特殊情况,以说明何时对该列的更改实际上是将其设置为 NULL,在此如果您可以让第一个视图在更改列时包含一个 NVL,如果它更改为 null,则您设置一个永远不会正常设置的值,然后在第二个视图中检查该值并将其转换回 null .

如果您想要该行如何查看每个更改,您可以以这样的方式构建视图,即对于每个更改,它 运行 与上面的每一列相同 select .

随着日志的更改 table,此解决方案的效率低于我原来的解决方案 table,但比我想到的其他解决方案要好得多。这个实际上非常适合您的需求。如果您喜欢我的想法但需要任何说明,请告诉我。

有几个选项; none 其中特别愉快,尤其是当您已经拥有大量数据时。

在我看来,最简洁的方法是接受时间在您的业务领域中起着关键作用,并将其融入您的模式设计而不依赖日志。 This is the academic basis for my recommendation - see also this answer 在 Stack Overflow 上。在您的情况下,我将向 VENDING_MACHINES:

添加 3 列
status int not null
valid_from datetime not null
valid_until datatime null

Status 跟踪机器是否处于活动状态或 "deleted"。您永远不会删除记录,您只需将它们的状态设置为 "deleted"。 valid_from 标记此记录有效的时刻; valid_until标记记录被覆盖的时刻。当某台自动售货机发生变化时,您将该自动售货机的 valid_until 设置为 getdate(),并插入一条新记录 valid_from 作为 getdate()。 这使您可以随时查看机器的状态;当前状态反映在所有行 where valid_until is null 中。您不再需要日志 table。 这种方法的缺点是您可能需要重写大量数据访问代码,并且所有连接都需要包含时间逻辑;如果您想在您的业务逻辑中反映时间(例如 "what was the value of unsold Snickers bars as per 1 January" 要求您知道当时有多少士力架,以及该日期士力架的价格),这很好。如果这真的很麻烦,您可以使用 valid_until is null and status = 1.

创建一个视图

下一个选项是修改您的触发器以适应此逻辑。我不太喜欢包含大量业务逻辑的触发器,因为性能影响可能是不可预测的table.

我将忽略我认为这是一个“XY 问题”的感觉,只回答这个问题:

[How do I] Reconstruct historical table rows, based only on change-log data[?]

(有关我怀疑可能是“真正”问题的方法,请参阅此 link 关于 Oracle 12c 中的闪回存档:https://docs.oracle.com/database/121/ADFNS/adfns_flashback.htm#ADFNS01004

对于你所得到的,我相信这是你正在寻找的查询(针对你的视图定义):

SELECT 
    c.id change_id,
    c.change_timestamp as_of_timestamp,
    c.vending_machine_id,
    NULLIF(last_value(case when c.modified_column_name = 'SNICKERS_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) snickers_count,
    NULLIF(last_value(case when c.modified_column_name = 'MILKY_WAY_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) MILKY_WAY_COUNT,
    NULLIF(last_value(case when c.modified_column_name = 'TWIX_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) TWIX_COUNT,
    NULLIF(last_value(case when c.modified_column_name = 'SKITTLES_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) SKITTLES_COUNT,
    NULLIF(last_value(case when c.modified_column_name = 'STARBURST_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) STARBURST_COUNT,
    NULLIF(last_value(case when c.modified_column_name = 'SWEDISH_FISH_COUNT' THEN nvl(c.new_number_value,-99999) ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),-99999) SWEDISH_FISH_COUNT,
    NULLIF(last_value(case when c.modified_column_name = 'FACILITIES_ADDRESS' THEN nvl(c.new_text_value,'#NULL#') ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),'#NULL#') FACILITIES_ADDRESS,
    NULLIF(last_value(case when c.modified_column_name = 'FACILITIES_CONTACT' THEN nvl(c.new_text_value,'#NULL#') ELSE NULL END) ignore nulls over ( partition by c.vending_machine_id order by c.change_timestamp asc range between unbounded preceding and current row),'#NULL#') FACILITIES_CONTACT
FROM 
    VENDING_MACHINE_CHANGE_LOG c
ORDER BY 
    c.vending_machine_id, c.change_timestamp;

基本上,你有三个问题:

  1. 您如何考虑可能存储在每列中的不同数据类型?
  2. 您如何解释 null 个值?
  3. 如何高效地进行查询 运行?

问题 #1 的答案是您手动为每个视图列编写逻辑,因此视图定义使用 NEW_NUMBER_VALUE 很简单,例如 SNICKERS_COUNT列并将 NEW_TEXT_VALUE 用于 FACILITIES_ADDRESS 列。

问题 #2 比较棘手。考虑 SNICKERS_COUNT 列。您需要忽略不是对 SNICKERS_COUNT 的更改。通过使它们 null 很容易忽略它们。但是,实际的变化值也可能是 null,我们不想忽略这些。因此,我们必须指定一个非 null 值来代表我们不想忽略的 null 值。这个指定值必须是一个永远不会出现在实际数据中的值。对于数字列,我选择了 -99999,对于文本列,我选择了“#NULL#”。

我忽略的问题 #3。您的问题的本质将要求您从一开始就阅读所有更改日志,以在给定的时间点建立它们的值。我没有看到你没有完整 table 扫描 VENDING_MACHINE_CHANGE_LOG

那么,让我们分解查询中的其中一列,看看它在做什么:

nullif(
  last_value(
     case when c.modified_column_name = 'SNICKERS_COUNT' 
          THEN nvl(c.new_number_value,-99999) 
          ELSE NULL END) 
  ignore nulls 
  over ( partition by c.vending_machine_id 
         order by c.change_timestamp asc 
         range between unbounded preceding and current row)
 ,-99999) snickers_count,

从这个内部表达式开始:

case when c.modified_column_name = 'SNICKERS_COUNT' 
              THEN nvl(c.new_number_value,-99999) 
              ELSE NULL END

如果修改的列不是SNICKERS_COUNT,则表达式是NULL。这是它可以为空的唯一方法。如果 new_number_valueNULL,我们将其转换为我们指定的替代 (-99999)。

然后,

last_value(...case expression above...)
  ignore nulls 
  over ( partition by c.vending_machine_id 
         order by c.change_timestamp asc 
         range between unbounded preceding and current row)

... 这告诉 Oracle 为 case 表达式取最近的非空值,“最近”被定义为具有最高 change_timestamp 的行集合的行与当前行相同 vending_machine_id,并且仅包括对当前行的更改。

最后,

nullif(... last_value expression above...
 ,-99999) snickers_count

这会将 null 的指定替代值转换回真实的 null

结果如下:

+-----------+---------------------------------+--------------------+----------------+-----------------+------------+----------------+-----------------+--------------------+--------------------+--------------------+
| CHANGE_ID |         AS_OF_TIMESTAMP         | VENDING_MACHINE_ID | SNICKERS_COUNT | MILKY_WAY_COUNT | TWIX_COUNT | SKITTLES_COUNT | STARBURST_COUNT | SWEDISH_FISH_COUNT | FACILITIES_ADDRESS | FACILITIES_CONTACT |
+-----------+---------------------------------+--------------------+----------------+-----------------+------------+----------------+-----------------+--------------------+--------------------+--------------------+
|       168 | 06-NOV-19 05.21.00.000000000 AM |                225 |                |                 |            |                |                 |                  1 |                    |                    |
|       180 | 06-NOV-19 07.31.00.000000000 AM |                225 |                |                 |            |                |                 |                  1 |                    | William            |
|       186 | 06-NOV-19 09.33.00.000000000 AM |                225 |                |                 |            |             11 |                 |                  1 |                    | William            |
|       187 | 06-NOV-19 09.45.00.000000000 AM |                225 |                |                 |            |             11 |                 |                  1 |                    |                    |
|       191 | 06-NOV-19 10.59.00.000000000 AM |                225 |                |                 |            |             11 |                 |                  1 |                    |                    |
|       193 | 06-NOV-19 11.34.00.000000000 AM |                225 |                |                 |            |              8 |                 |                  1 |                    |                    |
|       196 | 06-NOV-19 11.56.00.000000000 AM |                225 |                |                 |          2 |              8 |                 |                  1 |                    |                    |
|       177 | 06-NOV-19 07.00.00.000000000 AM |                349 |                |               3 |            |                |                 |                    |                    |                    |
|       179 | 06-NOV-19 07.11.00.000000000 AM |                349 |                |               3 |         15 |                |                 |                    |                    |                    |
|       184 | 06-NOV-19 09.04.00.000000000 AM |                349 |                |              10 |         15 |                |                 |                    |                    |                    |
|       194 | 06-NOV-19 11.39.00.000000000 AM |                349 |                |              10 |         15 |                |                 |                    |                    | Mark               |
|       167 | 06-NOV-19 05.18.00.000000000 AM |                481 |                |               5 |            |                |                 |                    |                    |                    |
|       169 | 06-NOV-19 05.40.00.000000000 AM |                481 |                |               5 |            |                |                 |                    |                    |                    |
|       170 | 06-NOV-19 05.49.00.000000000 AM |                481 |                |               5 |            |                |               4 |                    |                    |                    |
|       172 | 06-NOV-19 06.25.00.000000000 AM |                481 |                |               5 |            |                |               4 |                  7 |                    |                    |
|       173 | 06-NOV-19 06.40.00.000000000 AM |                481 |                |               5 |            |                |               4 |                  7 |                    | Audrey             |
|       178 | 06-NOV-19 07.03.00.000000000 AM |                481 |                |               5 |          8 |                |               4 |                  7 |                    | Audrey             |
|       182 | 06-NOV-19 08.28.00.000000000 AM |                481 |              0 |               5 |          8 |                |               4 |                  7 |                    | Audrey             |
|       183 | 06-NOV-19 08.38.00.000000000 AM |                481 |              0 |               5 |          8 |              7 |               4 |                  7 |                    | Audrey             |
|       185 | 06-NOV-19 09.21.00.000000000 AM |                481 |                |               5 |          8 |              7 |               4 |                  7 |                    | Audrey             |
|       188 | 06-NOV-19 10.16.00.000000000 AM |                481 |                |               5 |          8 |              7 |               4 |                  7 |                    | Lucy               |
|       189 | 06-NOV-19 10.25.00.000000000 AM |                481 |             10 |               5 |          8 |              7 |               4 |                  7 |                    | Lucy               |
|       192 | 06-NOV-19 11.11.00.000000000 AM |                481 |             10 |               5 |          8 |              7 |               6 |                  7 |                    | Lucy               |
|       171 | 06-NOV-19 06.09.00.000000000 AM |                576 |                |                 |            |                |                 |                    |                    |                    |
|       174 | 06-NOV-19 06.46.00.000000000 AM |                576 |             13 |                 |            |                |                 |                    |                    |                    |
|       175 | 06-NOV-19 06.55.00.000000000 AM |                576 |             13 |                 |            |                |                 |                    | 388 Holiday Street |                    |
|       176 | 06-NOV-19 06.59.00.000000000 AM |                576 |             13 |                 |            |                |                 |                    | 388 Holiday Street |                    |
|       181 | 06-NOV-19 07.49.00.000000000 AM |                576 |             13 |                 |            |                |                 |                    | 388 Holiday Street | Brian              |
|       190 | 06-NOV-19 10.57.00.000000000 AM |                576 |             13 |                 |            |                |                 |                 12 | 388 Holiday Street | Brian              |
|       195 | 06-NOV-19 11.42.00.000000000 AM |                576 |             13 |                 |            |              8 |                 |                 12 | 388 Holiday Street | Brian              |
+-----------+---------------------------------+--------------------+----------------+-----------------+------------+----------------+-----------------+--------------------+--------------------+--------------------+