根据变更日志数据重构历史表行

3
"I'm working on some complex sales analytics, which is very convoluted ... and boring ..."
所以为了回答这个问题,我将使用一个有趣、甜美的比喻:自动售货机。但我的实际表格结构是相同的。(你可以假设有大量的索引、约束等。)
  • 基本表格#1 - 库存
假设我们有一个包含自动售货机库存数据的表格。这个表格只是显示每台自动售货机中目前有多少种糖果,数量是多少。
我知道,通常会有一个ITEM_TYPE表格,其中包含'Snickers''Milky Way'等行,但由于多种原因,我们的表格并不是这样构建的。
实际上,这不是产品数量,而是预先聚合的销售数据:"Pipeline Total""Forecast Total"等。因此,我所要处理的只是一个简单的表格,其中有不同类型总数的单独列。
对于这个示例,我还添加了一些文本列,以演示我必须考虑各种数据类型。(这使事情变得复杂。)
除了ID之外,所有列都可为空-这是一个真正的问题。 就我们而言,如果该列为NULL,则NULL是我们需要用于分析和报告的官方值。

enter image description here

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 表中记录八行 - 每个非 ID 列都有一行。

在我的实际情况中,正在跟踪90多列。但通常只有一两列在任何给定时间更新,因此不会失控。
这个“更改日志”旨在成为“售货机”表的永久历史记录,因此我们不会创建外键约束-如果从“售货机”中删除行,则要保留孤立的历史记录在更改日志中。此外,Apex不支持“ON UPDATE CASCADE”,因此触发器必须检查对ID列的更新,并手动在相关表(例如更改日志)中传播更新。

enter image description here

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_MACHINE_CHANGE_LOG表中的数据,重构完整的历史VENDING_MACHINES表。
例如:由于更改日志行可以是孤立的,因此先前从VENDING_MACHINES删除的行应重新出现。
生成的视图应允许我检索任何VENDING_MACHINE行,正如它在历史上的任何特定时刻一样。

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

最终,我认为需要使用分析函数。
但我对SQL分析函数和Oracle以及Apex都很陌生。
因此,我不知道如何解决这个问题-重构原始表行的最佳方法是什么?

以下是期望结果的样子(按CHANGE_TIMESTAMP排序):

enter image description here

这里是相同的期望结果,此外还按VENDING_MACHINE_ID排序:

enter image description here

我已经构建了一个简单的查询来获取每个 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 表的唯一索引 - 这是有意设计的。

1
你在问题中展示的期望结果是完整的更改列表。你真的需要所有更改的完整集合,还是只需要重构表在特定时间点的样子(仅限于那个时刻)? - Vladimir Baranov
1
另一个问题。您说自动售货机可能会来来去去,即ID集合“VENDING_MACHINE_ID”不是固定的。那么列集合呢?您能否说您有一个固定的列集合“SNICKERS_COUNT,MILKY_WAY_COUNT,TWIX_COUNT,SKITTLES_COUNT,STARBURST_COUNT,SWEDISH_FISH_COUNT,FACILITIES_ADDRESS,FACILITIES_CONTACT”,从不改变?(如果确实更改,则可以根据需要更新SQL代码) - Vladimir Baranov
@VladimirBaranov 您是正确的,显然我会筛选出所需的时间窗口。但这就是为什么它是一个视图,所以我可以假装自己正在拉取整个列表,并理解它稍后会被过滤。 - Giffyguy
@VladimirBaranov,关于你的第二个问题,是的,这些列是固定的。如果我们在某个时候更改了这些列,我很乐意更新SQL以匹配它。我认为这方面没有实际的解决办法,这也不是一个问题。 - Giffyguy
@Tomm 那只是所需结果的一部分。我需要能够跟踪所有更新、插入和删除操作,因此如果某个列被覆盖,我们仍然可以记录旧值和新值的日志。这就是这个更改日志提供的功能。你不能用主表上的简单标志列来模拟它。但是对于这个问题,实际上我不需要任何人担心删除的行——那是我以后要解决的问题(其实我已经做了一些工作)。 - Giffyguy
显示剩余2条评论
4个回答

3
我会忽略我的感觉,认真回答这个问题:“如何仅基于变更日志数据重构历史表行?”(为了解决我怀疑的“真正”问题的方法,请参见有关Oracle 12c中Flashback Archives的此链接: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. 如何使查询运行高效?
第一个问题的解答是您手动编写每个视图列的逻辑,因此视图定义可以简单地使用 NEW_NUMBER_VALUE 作为 SNICKERS_COUNT 列的值,使用 NEW_TEXT_VALUE 作为 FACILITIES_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表达式,其中“最新的”被定义为具有与当前行相同的vending_machine_id的行集合中具有最高change_timestamp的行,并且仅包括到当前行的更改。

最后,

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              |
+-----------+---------------------------------+--------------------+----------------+-----------------+------------+----------------+-----------------+--------------------+--------------------+--------------------+

1
这看起来很不错。你的代码或多或少与我今天上午正在使用的代码相同——只是我一直在使用常规聚合函数,因为我对分析很差。(感谢你给了我一个好的分析示例!)我立即开始研究Flashback Archives,肯定会比我目前的垃圾代码更好。希望我能找出如何将Flashback Archives实现到Apex中... 尽管我理解Apex 应该支持所有Oracle DB功能?我会尽快找出答案。 - Giffyguy
1
@Giffyguy,我在APEX方面做得不多,但是这个链接似乎表明它内置了闪回查询的支持:https://docs.oracle.com/database/apex-5.1/AEEUG/executing-flashback-query.htm#AEEUG-GUID-E58A8DC7-555D-491D-A138-97B3FCFFBC5F。 - Matthew McPeak

2
我的建议是完全改变LOG表的结构,而不仅仅记录更改的列和更改内容。每次更新行时,将旧行插入到LOG表中,并附带一个标记(INSERT、UPDATE、DELETE)、时间戳和日志ID。
然后,当您想知道某个时间点表格的状态时,只需查询(或构建一个简单的视图以进一步简化)并选择所需日期之前的最大时间戳,以获取售货机的不同值。基本上,选择该售货机的最新日志条目,在您期望的日期之前(如果最新条目是删除,则不显示它)。
这种方法会大大简化事情,它会占用略多的空间(但现在空间很便宜),您的更新触发器可能会略微受到性能影响。更不用说这样也可以完美地处理插入和删除的行问题。但是,您对此表格进行的视图应该非常快速,我敢打赌,它比您使用当前日志表拼凑出的任何东西都要快得多。
如果您必须使用当前的日志表,那么我不确定视图是否适合。我认为您需要创建另一个与现有VENDING_MACHINES表相同的临时表,然后在输入要查询的日期时运行一些PLSQL。
然后我们遇到了一个问题,因为您的LOG表记录的是新值而不是旧值。
因此,我会运行一个PLSQL过程,在所需日期之后选择所有不同的更改(如果一个机器更新了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的视图中(选择该列的最新更改),您需要想出一种特殊情况,即当列的更改实际上是将其设置为NULL时,您可以在这种情况下使第一个视图包括一个NVL,如果将其更改为null,则设置一个通常不会设置的值,然后在第二个视图中检查该值并将其转换回null。
如果您想了解每次更改后行的外观,只需按照上述方式为每个更改构建相同的选择。
这种解决方案比我最初提出的更改日志表的方法效率低,但比我想到的其他方法要好得多。这个解决方案非常完美地符合您的需求。如果您喜欢我的想法但需要任何澄清,请告诉我。

1
谢谢!看起来非常有前途。我会立即开始研究它的。我会告诉你进展如何,或者如果我有任何问题。 - Giffyguy

1
当然可以做到,但是使用标准SQL无法高效地完成,因为您的表违反了关系数据库的基本规则。具体来说,您在一个表中有一列被另一个表中其名称的文本表示所引用。这是一个重要的关系,但未包含在关系元数据中。
既然无法高效地完成,问题就是您能容忍多少低效以及想要做出什么样的权衡?通常编写这些更改日志是因为认为完整的历史记录表太大,但也经常出现这种情况,即该决策是在很长时间之前做出的,现在我们已经牢固地进入“大数据”时代,曾经被视为“太大”的东西现在已经成为“没有问题”。
如果结果表的大小可接受,我会建议创建一个包含您感兴趣的信息的完全物化的表。
如果完整表太大,您是否可以通过减少时间戳的粒度使其大小合适?每天或每周只有一行而不是每次更新都有一行。
或者,您是否可以通过限制时间范围(例如仅限过去90天)来缩小大小?
如果这些选项中有任何一种适合您,那么您可以定期运行数据仓库处理(基本上是ETL过程)来创建和更新表格(例如每晚)。请注意,如果您不需要对其进行连接,则生成的表格不需要在同一数据库中。您还可以修改触发器或创建新触发器,在手动创建扩展表后使其保持最新状态。
否则,如果要动态执行此操作,将会非常困难,因为您必须在SQL中明确地将值映射到每个列的列。

明白了,是的,显式映射可能是必需的,这没问题。我们可以使用显式映射,因为我们不太可能经常(或从未)重新查看此代码。进一步回答你的问题,我们提供数据仓库作为解决方案,但老板否决了它。他说我们必须按照这种方式来做,所以我们就被卡在这里了。 - Giffyguy
1
@Giffyguy 老板到底否决了什么?我认为,如果生成的表是可管理的大小,用完全物化的表替换日志表是正确的方法。这只需要一次转换,再加上对更新表的触发器进行相对容易的更改。 - Old Pro
我完全同意。但是他们过去曾经有过这种解决方案,时当时非常有问题和不稳定,因此他们对它有偏见(不合理)。我刚刚和老板开了个会,告诉了他关于Oracle Flashbacks的事情,他对此更感兴趣(我也是)。我会向我们的数据库管理员提出一些请求,看看是否可以启用Flashbacks,并且如果可以的话,我们将尝试使用它。 - Giffyguy

1

有几种选择;其中没有一种特别愉快,尤其是如果你已经有了很多数据。

在我看来,最干净的方法是承认时间在你的业务领域中起着关键作用,并将其融入到模式设计中,而不依赖于日志记录。 是我建议的学术基础 - 另请参见 Stack Overflow 上的 此答案。在你的情况下,我会向 VENDING_MACHINES 添加 3 列:

status int not null
valid_from datetime not null
valid_until datatime null

状态跟踪机器是否处于活动状态或"已删除"状态。您从不删除记录,只需将其状态设置为"已删除"。 valid_from标记此记录有效的时刻;valid_until标记记录被覆盖的时刻。当售货机发生变化时,您将该售货机的valid_until设置为getdate(),并插入一个新记录,其中valid_fromgetdate()。 这使您能够在任何时间点上查看机器的状态;所有行valid_until为空反映当前状态。您不再需要日志表。 这种方法的缺点是您可能需要重写相当多的数据访问代码,并且所有连接都需要包括时间逻辑;如果您想在业务逻辑中反映时间(例如,“截至1月1日未售出的Snickers巧克力棒价值是多少”需要知道该时刻有多少Snickers巧克力棒以及那天Snickers的价格),那么这很好。如果这真的太麻烦了,您可以创建一个视图,其中valid_until为空且状态=1

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接