一个批量收集操作循环中的两个(或多个)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 COLLECTFORALL 语句?

【问题讨论】:

你的commit在哪里? 这个示例程序是一个大包的一部分 如果不重新引发过程中的错误,您不应捕获WHEN OTHERS。调用应用程序可能会捕获错误并显示一个很好的错误消息,但您正在运行的过程不应忽略该错误。 Log and Ignore 是一种错误的哲学。 如果commitrollback 位于此过程之外,那么为什么您只是拥有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的主要内容,如果未能解决你的问题,请参考以下文章

如何在批量收集循环中进行条件处理?

Oracle PLSQL BULK 收集和 For 循环

从 Oracle 游标批量收集列的子集

在同一个嵌套表上批量收集两次

oracle ORA-06502:PL/SQL:数字或值错误:批量绑定:截断绑定

如何批量收集到子查询中的 UDT 类型