防止日期间隔重叠并与 Oracle +11g 中的触发器保持一致

Posted

技术标签:

【中文标题】防止日期间隔重叠并与 Oracle +11g 中的触发器保持一致【英文标题】:Keeping date intervals from overlapping and consistent with trigger in Oracle +11g 【发布时间】:2014-10-05 10:39:40 【问题描述】:

假设我有以下内容:

    CREATE TABLE test (
    id NUMBER(10)
    , valid_from DATE
    , valid_to DATE,
    PRIMARY KEY (id, valid_from)
    );

    INSERT INTO test (id, valid_from) VALUES (1, '01/JAN/1900');
    INSERT INTO test (id, valid_from) VALUES (1, '01/JAN/1901');
    INSERT INTO test (id, valid_from) VALUES (1, '01/JAN/1902');
    INSERT INTO test (id, valid_from) VALUES (2, '01/JAN/1903');

输出:

            ID VALID_FROM VALID_TO 
    ---------- ---------- ---------
             1 01-JAN-01           
             1 01-JAN-02           
             2 01-JAN-03           
             1 01-JAN-00      

现在我需要一个触发器来保持 VALID_TO 字段与 VALID_FROM 一致,如下所示:

            ID VALID_FROM VALID_TO 
    ---------- ---------- ---------
             1 01-JAN-00  01-JAN-01
             1 01-JAN-01  01-JAN-02
             1 01-JAN-02           
             2 01-JAN-03 

我有一个计算 VALID_TO 并检查是否有任何记录需要更新的查询:

    WITH original AS (        
        SELECT id,
               valid_from,
               valid_to, 
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY valid_from DESC) seq  
        FROM test
    ), should_be AS (
        SELECT df.id,
               df.valid_from AS VALID_FROM, 
               dt.valid_from AS VALID_TO
        FROM original df
        LEFT OUTER JOIN original dt ON (df.id = dt.id 
                                        AND df.seq = dt.seq + 1)
    ), update_req AS (
        SELECT
            should_be.*, 
            CASE WHEN original.VALID_TO = should_be.VALID_TO OR (original.VALID_TO IS NULL AND should_be.VALID_TO IS NULL) THEN 'N' ELSE 'Y' END UPDATE_REQUIRED        
        FROM should_be 
        INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
    )
    SELECT * 
    FROM update_req
    ORDER BY id, valid_from

输出:

            ID VALID_FROM VALID_TO  UPDATE_REQUIRED
    ---------- ---------- --------- ---------------
             1 01-JAN-00  01-JAN-01 Y              
             1 01-JAN-01  01-JAN-02 Y              
             1 01-JAN-02            N              
             2 01-JAN-03            N        

我在触发器中使用此查询,以确保 VALID_TO 字段在错误时得到更新:

    CREATE OR REPLACE TYPE ID_COLLECTION_T AS TABLE OF NUMBER(10);

    CREATE OR REPLACE TRIGGER trg_test
    FOR DELETE OR INSERT OR UPDATE
    ON test REFERENCING NEW AS NEW OLD AS OLD
    COMPOUND TRIGGER

        l_changed_ids ID_COLLECTION_T := ID_COLLECTION_T(); -- initialize

        AFTER EACH ROW IS
        BEGIN
            -- Keep track of changed ids
            CASE
                WHEN INSERTING OR UPDATING THEN l_changed_ids.extend; l_changed_ids(l_changed_ids.last) := :NEW.id;
                WHEN DELETING OR UPDATING THEN l_changed_ids.extend; l_changed_ids(l_changed_ids.last) := :OLD.id; 
            END CASE;

        END
        AFTER EACH ROW;

        AFTER STATEMENT IS

            l_existing_inconsistencies VARCHAR2(1);

        BEGIN

            -- first we check whether the executed statement caused any VALID_TO inconsistencies
            WITH original AS (        
                SELECT id,
                       valid_from,
                       valid_to, 
                       ROW_NUMBER() OVER (PARTITION BY id ORDER BY valid_from DESC) seq  
                FROM test
            ), should_be AS (
                SELECT df.id,
                       df.valid_from AS VALID_FROM, 
                       dt.valid_from AS VALID_TO
                FROM original df
                LEFT OUTER JOIN original dt ON (df.id = dt.id 
                                                AND df.seq = dt.seq + 1)
            ), update_req AS (
                SELECT
                    should_be.*, 
                    CASE WHEN original.VALID_TO = should_be.VALID_TO OR (original.VALID_TO IS NULL AND should_be.VALID_TO IS NULL) THEN 'N' ELSE 'Y' END UPDATE_REQUIRED        
                FROM should_be 
                INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
                WHERE original.id MEMBER OF l_changed_ids -- we ONLY (!) want to search for inconsistencies for modified ids
            )
            SELECT CASE WHEN 'Y' IN (SELECT UPDATE_REQUIRED FROM update_req) THEN 'Y' ELSE 'N' END
            INTO l_existing_inconsistencies
            FROM DUAL;

           -- If there are inconsistencies, then we update the table.
           IF l_existing_inconsistencies = 'Y' THEN 

                MERGE INTO test o
                USING (
                        WITH original AS (        
                            SELECT id,
                                   valid_from,
                                   valid_to, 
                                   ROW_NUMBER() OVER (PARTITION BY id ORDER BY valid_from DESC) seq  
                            FROM test
                        ), should_be AS (
                            SELECT df.id,
                                   df.valid_from AS VALID_FROM, 
                                   dt.valid_from AS VALID_TO
                            FROM original df
                            LEFT OUTER JOIN original dt ON (df.id = dt.id 
                                                            AND df.seq = dt.seq + 1)
                        )
                        SELECT
                                should_be.*, 
                                CASE WHEN original.VALID_TO = should_be.VALID_TO OR (original.VALID_TO IS NULL AND should_be.VALID_TO IS NULL) THEN 'N' ELSE 'Y' END UPDATE_REQUIRED        
                        FROM should_be 
                        INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
                        WHERE original.id MEMBER OF l_changed_ids -- we ONLY (!) want to search for inconsistencies for modified ids
                ) n
                ON (o.id = n.id AND o.valid_from = n.valid_from AND n.UPDATE_REQUIRED = 'Y')
                WHEN MATCHED THEN UPDATE SET o.valid_to = n.valid_to;

            END IF;

        END
        AFTER STATEMENT;

    END trg_test;

现在触发器为插入/更新/删除的 id 保持数据一致:

    INSERT INTO test (id, valid_from) VALUES (1, '01/JAN/1899');

现在我们在测试表中找到以下内容:

            ID VALID_FROM VALID_TO 
    ---------- ---------- ---------
             1 01-JAN-99  01-JAN-00
             1 01-JAN-00  01-JAN-01
             1 01-JAN-01  01-JAN-02
             1 01-JAN-02           
             2 01-JAN-03       

这里的问题是 MEMBER OF 语句。它导致对其每个成员进行全表扫描。 在多更新/插入语句的情况下,有很多潜在的 id 被改变,所以 l_changed_ids 集合很大。 我无法优化成员: http://www.puthranv.com/search/label/Oracle%20Dynamic%20IN%20List http://www.oracle-developer.net/display.php?id=301

我试过了:

    对于许多单次插入/更新,使用 TABLE() 转换集合非常慢。 从循环中更新不一致的行是一个坏主意,因为对于 n 批量语句,这可能会递归地重新激活触发器 n 次。但是(!)使用循环非常快,因为每次都使用 id 上的索引。

我的问题是:

    是否有其他触发方法。 触发器必须适用于单个和批量更新/删除/插入语句。因此,如果存在不一致,则可能只有一个额外的更新语句被递归执行。 触发器根据 id 锁定行。 (这是后来的要求,但现在考虑一下可能会很有趣。)

UPDATE1:关于计算 VALIT_TO 日期的一些性能分析:

    -- Original query on 5mil records:  40 sec
    WITH original AS (        
        SELECT id,
               valid_from,
               valid_to, 
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY valid_from DESC) seq  
        FROM test
    ), should_be AS (
        SELECT df.id,
               df.valid_from AS VALID_FROM, 
               dt.valid_from AS VALID_TO
        FROM original df
        LEFT OUTER JOIN original dt ON (df.id = dt.id 
                                        AND df.seq = dt.seq + 1)
                                       ) select * from should_be


    -- TommCatt suggestion on 5mil records:  65 sec
    with Date_List as (
      select  t1.ID, t1.Valid_from as From_Date, Min( t2.Valid_from ) as To_Date 
      from    test t1
      left join test t2
        on    t2.id = t1.id
        and   t2.valid_from > t1.valid_from
      group by t1.ID, t1.Valid_from
    )
    select  id, from_date, to_date
    from    Date_List     


    -- TommCatt suggestion on 5mil records for 12c: untested


    -- a_horse_with_no_name suggestion on 5mil records:  10 sec WINNER!!
    SELECT id,
           valid_from,
           LEAD(valid_from, 1) OVER (PARTITION BY id ORDER BY valid_from ASC) valid_to
    FROM test
    -- EXEC Plan for the winner:
    ---------------------------------------------------------------------------------
    | Id  | Operation        | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
    ---------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT |              |  5106K|    63M| 22222   (1)| 00:04:27 |
    |   1 |  WINDOW BUFFER   |              |  5106K|    63M| 22222   (1)| 00:04:27 |
    |   2 |   INDEX FULL SCAN| SYS_C0011495 |  5106K|    63M| 22222   (1)| 00:04:27 |
    ---------------------------------------------------------------------------------                      

【问题讨论】:

【参考方案1】:

我碰巧在 KScope14 会议期间测试了 SQL 中的 MEMBER OF 性能。我在博客上写了一些关于结果的文章:

http://dspsd.blogspot.dk/2014/06/member-of-comparison-of-plsql-and-sql.html

尝试使用 TABLE 运算符替换 MEMBER OF,以将集合“转换”为“临时表”。要么是这样的:

FROM should_be 
INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
INNER JOIN TABLE(l_changed_ids) chg ON (chg.column_value = original.id)

或者像这样:

FROM should_be 
INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
WHERE original.id IN (select column_value from TABLE(l_changed_ids))

如果您对更改 id 的数量有大致的了解以添加基数提示,则可能对优化器有用:

FROM should_be 
INNER JOIN original ON (should_be.id = original.id AND should_be.valid_from = original.valid_from)
WHERE original.id IN (select /*+ cardinality(42) */ column_value from TABLE(l_changed_ids))

以上是直接在这里输入的未经测试的代码 - 我希望你能让它工作:-)


哦,对不起,我刚刚看到您尝试使用 TABLE 运算符的更新,但对您来说速度很慢......

【讨论】:

TABLE() 如果其他一切都失败了。【参考方案2】:

另一种基于触发的方法是在提交时使用物化视图刷新和对物化视图的约束。我见过一些讨论和论坛中提到的方法。这里给出了一个例子和一些讨论:

http://jeffkemponoracle.com/2012/08/30/non-overlapping-dates-constraint/

我自己没有尝试过,但可能值得研究一下?

【讨论】:

确实如此,但据我所知,这只是为了确保没有重叠。就我而言,我正在尝试计算和实现一个字段。因为该表有数百万行,并且计算 valid_to 的查询很复杂(按...分区),所以我认为不可能对 MV 进行快速刷新。但我会调查的。【参考方案3】:

您可以通过与任何其他计划相媲美的性能轻松地为自己的方式建模。当然,维护将大大减少。我自己使用这种技术,效果很好。

首先,当您有这样的 From/To 字段集时,您设置了我所说的 Row Spanning Dependency。从数据完整性的角度来看,这太可怕了。每次执行 DML 时,都必须执行至少两条语句。要插入新的有效日期记录,您必须找到“当前”记录并更新“to_date”,然后发出插入。任一日期的任何更新都只能通过两个更新语句来完成。而且,您可以清楚地看到,保持日期序列的有效性绝对是一场噩梦。

解决方案真的很简单。只有“有效”或“生效”日期而不是 From_Date 字段。完全删除 To_date 字段。现在,让我们规定,当 ID 在有效字段中的日期生效时,它一直有效,直到输入具有相同 ID 和更晚日期的另一行。第二行中的日期字段是该行生效的日期,但也是第一行失效(或不再有效——我更喜欢这个词)的时间点。

一个插入。完成!

因此,重叠和间隙变得不可能。您甚至不必检查它们。不可能!

但是有些人希望在他们的报告中看到“发件人”和“收件人”,对吧?没关系。 “From”和“To”在结果集中很好,它们只是臭气熏天的数据。所以这里是如何从数据中获取“From”和“To”:

with
Date_List( id, from_date, to_date )as(
  select  t1.ID, t1.Valid as From_Date, Min( t2.Valid ) as To_Date 
  from    test t1
  left join test t2
    on    t2.id = t1.id
    and   t2.valid > t1.valid
  group by t1.ID, t1.Valid
)
select  id, from_date, to_date
from    Date_List
order by id, From_date desc;

在 cte 中,您正在加入 PK 到 PK - 非常快。在 cte 之外,您可能必须再次加入该表以获取其他数据,我相信您为了清楚起见而省略了这些数据。这仍然会很快,因为您再次加入 PK 领域。拿到 Oracle-12c 后,可以这样重写:

select  t1.id, t1.valid as from_date, t2.valid as To_date -- t1.etc, ...
from    test t1
left join test t2
  on    t2.id = t1.id
  and   t2.valid =(
            select  Min( t3.valid )
            from    test t3
            where   t3.id = t1.id
                and t3.valid > t1.valid )
order by t1.id, t1.valid desc;

对于连接和子查询,这在某些方面看起来更糟。然而,计时测试将显示出令人印象深刻的结果。但是,即使在表中物理添加一个 To_Date 字段会稍微提高性能,请记住我之前所说的:间隙和重叠是不可能的!!!如果你尝试过,你甚至不能把它搞砸。您可以想象的最糟糕的情况是为同一个 ID 输入相同的日期两次,但是由于这些定义了 PK,系统不会让您这样做。想想你不必编写的所有触发器、约束和存储过程(无论如何不要保持日期同步)!

【讨论】:

您是否尝试在 CTE 中使用 lead() 来获取 to_date?根据我的经验,这几乎总是比自我加入更快。 @TommCatt 谢谢你的帖子。我完全同意你对“dropping the valid_to and only using valid(_from)”的看法。但这是一个拥有数百万历史数据的 DW,技术负责人坚持 (!!) 在 INSERT/UPDATE/DELETE 上实现计算的 valid_to 字段。你和@a_horse_with_no_name 确实给了我一些重写/优化查询的好线索。谢谢你!当我有更多数据时,我会更新我的问题。 @a_horse_with_no_name:我在测试时没有发现,但现在我想起来,那是大约 5 年前的事了。也许从那时起,分析的性能得到了改善。 OP:你为什么要在 DW 表上编写触发器?您还在 DW 中进行实时操作吗?无论如何,仅仅因为你有“TO”并不意味着你必须使用它。从自联接或 LEAD 函数构建物化视图,根本不用担心“TO”日期。那么某些日期序列会不准确吗?当然可以,但是数据库对准确性无能为力,只有完整性。诚信第一。

以上是关于防止日期间隔重叠并与 Oracle +11g 中的触发器保持一致的主要内容,如果未能解决你的问题,请参考以下文章

将表中的单个值分配给 ORACLE PL/SQL 中声明的变量时出错

用于 oracle 11g 的 PL/SQL 中的嵌入式脚本 [重复]

Oracle 9i和Oracle 11g中的 i 和 g 分别是啥意思?

docker中的oracle-11g-安装配置

从 Oracle 中的日期获取月份名称

更新 Oracle SQL 表中的日期