3

我正在研究一些复杂的销售分析,这非常令人费解......而且很无聊......

所以对于这个问题,我将使用一个有趣的、含糖的比喻:自动售货机
但是我的实际表格的结构是相同的。
(您可以假设有很多索引、约束等)

  • 基本表 #1 - 库存

假设我们有一个包含自动售货机库存数据的表。
这张表简单地显示了目前每台自动售货机中每种糖果的数量。

我知道,通常会有一个ITEM_TYPE包含'Snickers''Milky Way'等行的表格,但由于多种原因,这不是我们表格的构造方式。
实际上,这不是产品数量,而是预先汇总的销售数据:"Pipeline Total""Forecast Total"等。
因此,我需要一个简单的表格,其中包含不同“类型”总计的单独列一起工作。

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

除了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
  • 基本表 #2 - 更改日志

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

重要的是,每当更新VENDING_MACHINES表中的记录时,都会执行一个触发器,将每个单独的更改记录在一个单独的日志表中VENDING_MACHINES_CHANGE_LOG
这个触发器已经写好了,效果很好。
(如果使用已存在的相同值“更新”列,则触发器忽略更改。)

为表中修改的每一VENDING_MACHINES记录一个单独的行(除了ID)。
因此,如果在表中插入一个全新的行VENDING_MACHINES(即新的自动售货机),VENDING_MACHINES_CHANGE_LOG表中将记录 8 行 - 每个非 ID 列对应VENDING_MACHINES.

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

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

在此处输入图像描述

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表,只使用VENDING_MACHINE_CHANGE_LOG表中的数据。
即,由于允许孤立更改日志行,因此之前已删除的行VENDING_MACHINES应该重新出现。
结果视图应该允许我检索任何VENDING_MACHINE行,就像它在历史上的任何特定点存在一样。

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

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

期望的结果如下所示(按 排序CHANGE_TIMESTAMP):

在此处输入图像描述

这是相同的期望结果,另外按以下方式排序VENDING_MACHINE_ID

在此处输入图像描述

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

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表的唯一索引 - 这是设计使然。

4

4 回答 4

3

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

[如何] 仅基于更改日志数据重建历史表行[?]

(对于我怀疑可能是“真正”问题的方法,请参阅有关 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_VALUEFACILITIES_ADDRESS

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

问题 #3 我忽略了。您的问题的本质是要求您从一开始就阅读所有更改日志,以建立它们作为给定时间点的值。如果没有对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回 true 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              |
+-----------+---------------------------------+--------------------+----------------+-----------------+------------+----------------+-----------------+--------------------+--------------------+--------------------+
于 2019-12-02T16:54:44.000 回答
2

我的建议实际上是完全更改 LOG 表,而不仅仅是记录您更改的列以及您在那里更改的内容。每次更新一行时,您将旧行插入到 LOG 表中,并带有一个用于 INSERT、UPDATE、DELETE 的标记、一个时间戳和一个 log_id。

然后,当您想知道某个时间的表状态时,您只需执行一个查询(或构建一个简单的视图以进一步简化此操作),然后为不同的自动售货机选择所需日期之前的最大时间戳。基本上,选择该自动售货机的最新日志条目,它仍然在您想要的日期之前(如果最近的条目被删除,则不要显示它)。

这种做事方式将大大简化事情,它会占用更多的空间(但现在空间很便宜),并且您的更新触发器可能会带来轻微的性能提升。更不用说这也可以完美地处理插入和删除的行问题。但是您在此表上创建的视图应该非常快,而且我敢打赌它会比您使用当前日志表拼凑的任何东西快得多。

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

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

所以我要做的是运行一个 PLSQL 程序,选择您想要的日期之后的所有不同更改(如果一台机器更新 snickers 计数 13 次,只取其中之一),以便找到自您想要的日期以来发生的所有更改。然后查找该列在所需日期之前最后一次更新或插入的时间,并从那里获取值。这将需要一些动态的 SQL 魔法,这将是编码和运行的痛苦。

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

我强烈建议您使用第一种方法来更改 LOG 表的形成方式。因为这会更简单,更容易实现,运行速度也会更快。

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

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 表,具有不同的 vending_machine_ids,但对于每一列,您从时间戳为最新且值不为空的视图中选择此列中的值(选择对该列的最近更改列),您将需要以某种方式找出一个特殊情况,即对列的更改实际上是将其设置为 NULL,在这种情况下,您可以让第一个视图在列更改时包含 NVL,如果它已更改为 null 您设置了一个永远不会正常设置的值,然后在第二个视图中检查该值并将其转换回 null。

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

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

于 2019-12-02T09:58:31.033 回答
1

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

因此,鉴于它不能有效地完成,问题是您可以容忍何种程度的低效率以及您想要做出哪些权衡?通常写这种变更日志是因为考虑到完整的历史表太大,但也往往是很久以前就做出了这个决定,而现在我们已经坚定地处于“大数据”过去的“太大”现在“没问题”。

  • 如果结果表的大小是可以容忍的,我倾向于创建一个包含您感兴趣的信息的完全物化表。
  • 如果全表太大了,能不能通过降低时间戳的粒度来使其成为合适的大小呢?每天或每周一行,而不是每次更新一行。
  • 或者,您是否可以通过限制时间范围(例如仅过去 90 天)来减小大小?

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

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

于 2019-12-01T23:43:33.903 回答
1

有几个选项;它们都不是特别令人愉快的,特别是如果您已经获得了大量数据。

在我看来,最简洁的方法是接受时间在您的业务领域中起着关键作用,并将其融入您的架构设计中,而不依赖于日志。是我推荐的学术基础 - 另请参阅Stack Overflow 上的这个答案。在您的情况下,我将添加 3 列到VENDING_MACHINES

status int not null
valid_from datetime not null
valid_until datatime null

Status跟踪机器是处于活动状态还是“已删除”。您永远不会删除记录,您只需将它们的状态设置为“已删除”。valid_from标记该记录的有效时间;valid_until标记记录被覆盖的时刻。当自动售货机发生变化时,您valid_until将该自动售货机的 设置为,并使用asgetdate()插入一条新记录。这使您可以随时查看机器的状态;当前状态由所有行反映valid_fromgetdate()where valid_until is null. 您不再需要日志表。这种方法的缺点是您可能需要重写相当多的数据访问代码,并且您的所有连接都需要包含时间逻辑;如果您想在业务逻辑中反映时间(例如,“截至 1 月 1 日,未售出的士力架条的价值是多少”要求您知道当时有多少条士力架,以及士力架的价格是多少,这很好。是在那个日期)。如果这真的太麻烦了,您可以使用valid_until is null and status = 1.

下一个选项是修改触发器以适应此逻辑。我不是具有大量业务逻辑的触发器的忠实拥护者,因为性能影响可能无法预测。

于 2019-12-02T11:05:06.807 回答