一个批量收集操作循环中的两个(或多个)DML
Posted
技术标签:
【中文标题】一个批量收集操作循环中的两个(或多个)DML【英文标题】:Two (or more) DMLs inside one bulk collect operation loop 【发布时间】:2013-01-29 14:46:51 【问题描述】:Oracle 11g 上的 BULK COLLECT 逻辑有问题。
存储过程中的原始逻辑是:
PROCEDURE FOO(IN_FOO IN VARCHAR2) IS
BEGIN
FOR CUR IN (SELECT COL1,COL2,COL3 FROM SOME_TABLE) LOOP
INSERT INTO OTHER_TABLE (C1,C2,C3) VALUES (CUR.COL1,CUR.COL2,CUR.COL3);
UPDATE THIRD_TABLE T SET T.C_SUM = CUR.COL2 + CUR.COL3 WHERE T.C_ID = CUR.COL1);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERROR || ': ' || SQLERRM);
END FOO;
但我想使用BULK COLLECT
功能。
我写了类似的东西:
PROCEDURE FOO_FAST(IN_FOO IN VARCHAR2) IS
CURSOR CUR IS SELECT COL1,COL2,COL3 FROM SOME_TABLE;
TYPE RT_CUR IS TABLE OF CUR%ROWTYPE;
LT_CUR RT_CUR;
DML_EXCEPTION EXCEPTION;
PRAGMA EXCEPTION_INIT(DML_EXCEPTION, -24381);
BEGIN
OPEN CUR;
LOOP
FETCH CUR BULK COLLECT INTO LT_CUR LIMIT 1000;
EXIT WHEN LT_CUR.COUNT = 0;
BEGIN
FORALL I IN 1 .. LT_CUR.COUNT
INSERT INTO OTHER_TABLE (C1,C2,C3) VALUES (LT_CUR(I).COL1,LT_CUR(I).COL2,LT_CUR(I).COL3);
FORALL I IN 1 .. LT_CUR.COUNT
UPDATE THIRD_TABLE T SET T.C_SUM = LT_CUR(I).COL2 + LT_CUR(I).COL3 WHERE T.C_ID = LT_CUR(I).COL1);
EXCEPTION
WHEN DML_EXCEPTION THEN
FORALL I IN 1 .. SQL%BULK_EXCEPTIONS(1).ERROR_INDEX-1
UPDATE THIRD_TABLE T SET T.C_SUM = LT_CUR(I).COL2 + LT_CUR(I).COL3 WHERE T.C_ID = LT_CUR(I).COL1);
DBMS_OUTPUT.PUT_LINE(SQLERRM(-SQL%BULK_EXCEPTIONS(1).ERROR_CODE));
RETURN;
END;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERROR || ': ' || SQLERRM);
END FOO_FAST;
这是解决这个问题的好方法吗?
如果我要执行更多 DML 怎么办?
好的。我的问题更复杂,但我想简化它并用漂亮的示例代码丰富它。错误OTHERS
处理不是这个问题的一部分。也许这会更清楚:
这是怎么回事:
FOR CUR IN (SELECT COL1,COL2,COL3 FROM SOME_TABLE) LOOP
INSERT INTO OTHER_TABLE (C1,C2,C3) VALUES (CUR.COL1,CUR.COL2,CUR.COL3);
UPDATE THIRD_TABLE T SET T.C_SUM = CUR.COL2 + CUR.COL3 WHERE T.C_ID = CUR.COL1);
END LOOP;
更改为BULK COLLECT
和FORALL
语句?
【问题讨论】:
你的commit
在哪里?
这个示例程序是一个大包的一部分
如果不重新引发过程中的错误,您不应捕获WHEN OTHERS
。调用应用程序可能会捕获错误并显示一个很好的错误消息,但您正在运行的过程不应忽略该错误。 Log and Ignore 是一种错误的哲学。
如果commit
和rollback
位于此过程之外,那么为什么您只是拥有return
而没有重新上升的异常?如何通知来电者发生的事情?
为什么要优化 CURSOR FOR?在 10g 以下,它们经过内部优化,可以以与 BULK COLLECT 相似的速度运行。见:oracle.com/technetwork/issue-archive/2008/08-mar/…
【参考方案1】:
某个东西是否是“好方法”是非常主观的——这取决于您要比较的对象。
如果我们假设您对 some_table
的查询没有谓词,那么在集合中工作而不是进行任何类型的循环几乎肯定会更有效(除了代码少得多)
PROCEDURE FOO(IN_FOO IN VARCHAR2) IS
BEGIN
INSERT INTO other_table( c1, c2, c3 )
SELECT col1, col2, col3
FROM some_table;
UPDATE third_table tt
SET tt.c_sum = (SELECT st.col2 + st.col3
FROM some_table st
WHERE tt.c_id = st.col1)
WHERE EXISTS( SELECT 1
FROM some_table st
WHERE tt.c_id = st.col1);
END;
通常,WHEN OTHERS
异常处理程序是个坏主意。捕获异常只是为了尝试将其写入DBMS_OUTPUT
,调用者将不知道发生了错误,错误堆栈丢失,并且无法保证调用应用程序甚至为数据分配了缓冲区被写入是一个等待发生的错误。如果您的系统中有此类代码,您将不可避免地最终会努力追寻重现错误,因为某处的某些代码遇到并吞下了异常,导致后面的代码以意想不到的方式失败。
【讨论】:
我非常尊重您的知识,但处理OTHER
异常不在主题范围内。我只想知道如何使用 FORALL 语句处理两个(或多个)DML 操作【参考方案2】:
你原来的错误管理过程有问题,这使得很难将逻辑转换为批量处理。
基本上你的第一个过程的逻辑是:在一个循环中运行这两个语句,在你第一次遇到错误或光标结束时成功退出,以先发生者为准。
这不是正确的事务逻辑。如果您的两个语句协同工作而第二个语句失败,则第一个语句不会回滚!
您可能想要做的是:循环运行这两个语句;如果遇到错误,记录信息并撤消更改,如果没有成功退出。在 PL/SQL 中撤消更改非常容易,您只需要让错误传播:
PROCEDURE FOO(IN_FOO IN VARCHAR2) IS
BEGIN
FOR CUR IN (SELECT COL1,COL2,COL3 FROM SOME_TABLE) LOOP
BEGIN
INSERT INTO OTHER_TABLE (C1,C2,C3) VALUES (CUR.COL1,CUR.COL2,CUR.COL3);
UPDATE THIRD_TABLE T SET T.C_SUM = CUR.COL2 + CUR.COL3
WHERE T.C_ID = CUR.COL1;
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(cur.col1/*...*/); -- log **useful** debug info
RAISE;-- very important for transactional logic
END;
END LOOP;
END;
顺便说一句,DBMS_OUTPUT
不是最好的日志记录工具,您可能需要创建一个日志记录表和一个自治事务过程来插入相关的错误消息和标识符。
如果您想通过批量逻辑转换上述过程,您最好的做法是使用Justin Cave 描述的方法(单个 DML 语句)。使用批量数组时,如果要记录单个异常,则需要使用 SAVE EXCEPTIONS
子句。不要忘记重新提出错误。这应该有效:
PROCEDURE foo_fast(in_foo IN VARCHAR2) IS
CURSOR cur IS
SELECT col1, col2, col3 FROM some_table;
TYPE rt_cur IS TABLE OF cur%ROWTYPE;
lt_cur rt_cur;
dml_exception EXCEPTION;
PRAGMA EXCEPTION_INIT(dml_exception, -24381);
BEGIN
OPEN cur;
LOOP
FETCH cur BULK COLLECT
INTO lt_cur LIMIT 1000;
EXIT WHEN lt_cur.COUNT = 0;
BEGIN
FORALL i IN 1 .. lt_cur.COUNT SAVE EXCEPTIONS -- important clause
INSERT INTO other_table (c1, c2, c3)
VALUES (lt_cur(i).col1, lt_cur(i).col2, lt_cur(i).col3);
FORALL i IN 1 .. lt_cur.COUNT SAVE EXCEPTIONS --
UPDATE third_table t SET t.c_sum = lt_cur(i).col2 + lt_cur(i).col3
WHERE t.c_id = lt_cur(i).col1;
EXCEPTION
WHEN dml_exception THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
dbms_output.put_line('error '||i||':'||
SQL%BULK_EXCEPTIONS(i).error_code);
dbms_output.put_line('col1='||
lt_cur(SQL%BULK_EXCEPTIONS(i).error_index).col1);-- 11g+
END LOOP;
raise_application_error(-20001, 'error in bulk processing');
END;
END LOOP;
END foo_fast;
【讨论】:
您的解决方案有错误。在原始语句中,当INSERT
引发异常(例如唯一约束)时,将不会处理更新,并且不会在循环中进行任何进一步的插入和更新操作。循环中的处理将停止。先生,在您的解决方案中,当您通过SAVE EXCEPTIONS
收集异常时,将不会处理任何更新,并且除了错误的一个之外的所有插入。
@WBAR:似乎第二种情况下的 RAISE 没有达到我的预期(回滚待处理的更改),我用RAISE_APPLICATION_ERROR
进行了测试,现在它运行正常:任何错误都会使过程撤消其 DML。这些过程现在是原子的:它们要么完全失败而不改变数据库的状态,要么成功完成。
但是您通过收集 BULK EXCEPTIONS 获得了 +1 的努力和不错的提示【参考方案3】:
我通过使用这种流程找到了解决方案:
PROCEDURE FOO_FAST(IN_FOO IN VARCHAR2) IS
CURSOR CUR IS SELECT COL1,COL2,COL3 FROM SOME_TABLE;
TYPE RT_CUR IS TABLE OF CUR%ROWTYPE;
LT_CUR RT_CUR;
DML_EXCEPTION EXCEPTION;
PRAGMA EXCEPTION_INIT(DML_EXCEPTION, -24381);
BEGIN
OPEN CUR;
LOOP
FETCH CUR BULK COLLECT INTO LT_CUR LIMIT 1000;
EXIT WHEN LT_CUR.COUNT = 0;
BEGIN
FORALL I IN 1 .. LT_CUR.COUNT SAVE EXCEPTIONS
INSERT INTO OTHER_TABLE (C1,C2,C3) VALUES (LT_CUR(I).COL1,LT_CUR(I).COL2,LT_CUR(I).COL3);
EXCEPTION
WHEN DML_EXCEPTION THEN
FOR I IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
DBMS_OUTPUT.PUT_LINE(SQLERRM(-SQL%BULK_EXCEPTIONS(1).ERROR_CODE));
LT_CUR.DELETE(SQL%BULK_EXCEPTIONS(1).ERROR_INDEX);
END;
FORALL I IN INDICES OF LT_CUR
UPDATE THIRD_TABLE T SET T.C_SUM = LT_CUR(I).COL2 + LT_CUR(I).COL3 WHERE T.C_ID = LT_CUR(I).COL1);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERROR || ': ' || SQLERRM);
END FOO_FAST;
在这个流程中:
INSERT
中发生的所有异常都将存储在 SQL%BULK_EXCEPTIONS
集合中
每个异常都将由DBMS_OUTPUT.PUT_LINE
记录(在现实生活中由AUTONOMOUS TRANSACTION
过程记录在日志表中)
LT_CUT
的每个错误索引将被DELETE
方法在收集时删除。
UPDATE
中只会使用“好”行,因为 INDICES OF
子句允许通过删除对特定元素的引用对稀疏集合进行批量操作
【讨论】:
如果 UPDATE 发生错误,程序将成功退出,只完成一半的工作,太棒了:) 我的UPDATE
用于设置线路已被处理,因此在我的情况下完美运行。 :)以上是关于一个批量收集操作循环中的两个(或多个)DML的主要内容,如果未能解决你的问题,请参考以下文章