【问题标题】:PL/SQL Error: A loop that contains DML statements should be refactored to use BULK COLLECT and FORALLPL/SQL 错误:应重构包含 DML 语句的循环以使用 BULK COLLECT 和 FORALL
【发布时间】:2016-11-08 06:39:28
【问题描述】:

我已经在整个互联网上搜索了一些示例,但我仍然无法理解为什么我不能在此光标内使用 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 来显示它们。但是如果已经存储到变量中,那我为什么不能直接存储到表中(这不是数十亿的批量数据,甚至不是 1000 条记录!)。

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

【问题讨论】:

  • 您确定这是一个错误而不是建议吗?我是 TOAD 规则 4809 的建议。错误的编号是多少?
  • 你是对的。我想提一下使用 TOAD。我希望这是一个建议,但是当我添加那部分时,我到处都是错误。让我多看看!
  • 哦,哇,我的插入语句末尾缺少分号
  • 这个可以优化。只告诉我这里为什么是 OR(DBNode1 = var_dbnode1 OR DBNode2 = var_dbnode2)以及第二个查询是否使用同一个表
  • 欢迎回答问题,以便我关闭它。我本可以发誓规则得到执行,但很高兴知道他们没有!至少现在它正在工作。查询需要优化,真的。正在努力:)

标签: oracle bulkinsert dml


【解决方案1】:

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

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

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

(这相当于逐行处理)

或者:

  1. 把你的独轮车推到砖堆上
  2. 在您的手推车上装上适合和/或您可以携带的砖块
  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 - 全局临时表 - 而不是普通的堆表) - 你能不能在一个单个 SQL 语句,使用上面的 select 语句而不是将数据插入到 GTT 中?

【讨论】:

    【解决方案2】:

    这不是错误,而是编号为 Rule 4809 的 TOAD 建议。

    附:如果两个查询中的表相同,则可以使用

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

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

    【讨论】:

      猜你喜欢
      • 2011-12-31
      • 2020-10-18
      • 1970-01-01
      • 2013-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多