PL/SQL Error: A loop that contains DML statements should be refactored to use BULK COLLECT and FORALL

PL/SQL Error: A loop that contains DML statements should be refactored to use BULK COLLECT and FORALL

我已经在整个 Internet 上搜索了一些示例,但我仍然无法理解为什么我不能在该游标内使用 DML 语句。我有点想念它背后的理论,但我不会否认如何正确编写这个的例子也会让我的生活更轻松。这是我正在处理的查询(注意:我删除了未找到结果时退出,如果游标已经打开则关闭以及类似的事情只是为了关注这里的要点):

DECLARE
    // lots of vars

    // the cursor below gets all datasources connected to Node XXYZ123
    CURSOR DataSourceCheck
    IS
        SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
        FROM SCHEMA.TABLENAME
        WHERE NODENAAM = 'XXYZ123';

    // this cursor will execute row-by-row based on the result set of above cursor
    CURSOR CheckIfOnlyDataSource
    IS
        SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
        FROM SCHEMA.TABLENAME
        WHERE DBUSERNAAM = var_dbusernaam AND (DBNode1 = var_dbnode1 OR DBNode2 = var_dbnode2);

BEGIN  
    OPEN DataSourceCheck;   
    LOOP
        FETCH DataSourceCheck into var_nodenaam, var_naam, var_URL, var_dbnode1, var_dbnode2, var_dbusernaam, var_dbnaam;

        var_rowcount:= 0;
        OPEN CheckIfOnlyDataSource;     
            LOOP
                FETCH CheckIfOnlyDataSource into var_nodenaam2, var_naam2, var_URL2, var_dbnode12, var_dbnode22, var_dbusernaam2, var_dbnaam2;
                var_rowcount:= var_rowcount + 1;
            END LOOP;  

            // only save result in a temp table when var_rowcount is 1 and not higher.
            IF var_rowcount = 1
                THEN
                    INSERT INTO global_temp_table
                    (t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
                    VALUES
                    (var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
            END IF;
        CLOSE CheckIfOnlyDataSource;
    END LOOP;    
END;

失败点是这部分,消息是 DML 应该重新配置为 FORALL 或 BULK INTO 语句:

IF var_rowcount = 1
    THEN
        INSERT INTO global_temp_table
        (t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
        VALUES
        (var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
END IF;

我不明白为什么 DML 不能在逐行方法中工作?输出显然存储在变量 var_dbusernaam2var_nodenaam2var_dbnode12var_dbnode22 中,因此我可以执行 dbms_output.put_line 来显示它们。但是,如果它已经存储到变量中,那么为什么我不能将它简单地存储到一个 table 中(这不是数十亿的批量数据,甚至不是 1000 条记录!)。

没有简单的解决方法吗?我尝试了 BULK COLLECT 和 FORALL,但我需要更多时间来理解它并正确查询 - 游标中的游标肯定不会让它变得更容易。

这不是错误,而是编号规则 4809 的 TOAD 建议。

P.S。如果两个查询中的 table 相同,则可以使用

..., COUNT(*) OVER (PARTITION BY DBNODE1, DBNODE2, DBUSERNAAM) c 

在第一个查询中获取每个 DBNODE1、DBNODE2、DBUSERNAAM 的行数,不需要第二个。

除了 Mottor 的回答中的建议之外,Toad 标记您的代码的原因是逐行处理速度慢。 PL/SQL 和 SQL 引擎之间有很多上下文切换。

把它想象成在你家附近建造新墙 - 如果砖块被送到驱动器的底部,你会不会:

  1. 去砖堆
  2. 捡起一块砖
  3. 回到你的墙上
  4. 把砖块加到墙上
  5. 返回步骤 1 并重复直到墙完成

(相当于逐行处理)

或者:

  1. 把你的手推车推到砖堆旁
  2. 在您的手推车上装上您可以携带的砖块and/or
  3. 把手推车推回墙边
  4. 把每块砖都加到墙上
  5. 返回步骤 1 并重复直到墙完成。

(相当于批量处理)

当然,如果您够精明,您可以首先将砖块送到墙边,从而避免上述场景中所需的所有行走和搬运。 (这相当于基于集合的处理)。

将您的程序转变为基于集合的方法(结合 Mottor 的回答)将使它变得简单:

declare
    -- lots of vars
begin
  insert into global_temp_table (t_dbusernaam,
                                 t_nodenaam,
                                 t_dbnode1,
                                 t_dbnode2,
                                 t_distinctcount)
  select dbusernaam,
         nodenaam,
         dbnode1,
         dbnode2,
         cnt
  from   (select nodenaam,
                 naam,
                 url,
                 dbnode1,
                 dbnode2,
                 dbusernaam,
                 dbnaam,
                 count(*) over (partition by dbnode1, dbnode2, dbusernaam) cnt
          from   schema.tablename
          where  nodenaam = 'XXYZ123')
  where  cnt = 1;
end;
/

这样做的好处是比您的原始代码更紧凑,更易于阅读、理解和调试。另外,您可以 运行 select 语句在过程之外单独使用 - 更容易看到它在做什么。

它也将比您原来的循环两个游标的方法更快(顺便说一下,这是重新发明嵌套循环连接——数据库在纯 SQL 中进行了优化。 .而且可能不是最快的连接方式,如果你一直坚持保持连接!)。

我也想知道为什么您需要将行插入 GLOBAL_TEMP_TABLE(我怀疑这是 GTT - 全局临时 table - 而不是普通堆 table) - 你能不能在单个 SQL 语句中进行后续处理,使用上面的 select 语句而不是将数据插入 GTT?