【发布时间】: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 |
---------------------------------------------------------------------------------
【问题讨论】:
标签: sql oracle plsql triggers oracle11g