【问题标题】:Oracle ORA-04030 even when using Bind Variables in a LOOPOracle ORA-04030 即使在循环中使用绑定变量
【发布时间】:2018-01-22 11:05:07
【问题描述】:

我必须使用 PL/SQL 从远程表中删除近 5 亿行。由于 UNDO 表空间无法处理该卷,因此删除以 1,000,000 的批次大小进行并提交。另外 - 为了减少硬解析,我使用以下语法使用绑定变量:

str := 'delete from table@link where id >= :x and id < :y';
execute immediate str using start_id, start_id+1000000

并且在每次调用之后,start_id 递增 1000000,直到 SQL%rowcount 返回 0(零)并达到 end_id(已知)。

但是进程得到ORA-0430如下:

ORA-04030: out of process memory when trying to allocate 16408 bytes (QERHJ hash-joi,QERHJ Bit vector)

ORA-04030: out of process memory when trying to allocate 41888 bytes (kxs-heap-c,temporary memory)

请注意,我已经在使用绑定变量,因此在第一次执行后没有硬解析。

一件事可能是目标 ID 的范围。假设前几行按升序排列,则 ID 为

100,000,000,000
200,000,000,000
50,000,000,000,000,000
50,000,000,000,011,111

在第二次迭代中,从 200,000,000,000 到 200,000,100,000 的 ID 将被删除。

但由于此范围内没有 ID,因此将需要近 50,000,000,000 次迭代才能到达下一行(50,000,000,000,000,000 / 1000000 = 50,000,000,000)。

当然 - 我总是可以检查目标的 ID 并选择正确的范围(比默认的 100 万大得多)。

但这不应该是进程耗尽内存的情况。

添加代码:

remote.sql:远程执行:

create table test1
(
    id      number(38) primary key
); 
insert into test1 select level from dual connect by level < 1000000;
insert into test1 values ( 1000000000000 );
insert into test1 values ( 2000000000000 );
commit;
exec dbms_stats.gather_table_stats ( ownname => user, tabname => 'test1',           
     cascade => true, estimate_percent => 100 );
commit; 

local.sql:

create or replace procedure batch_del
as
     l_min_val       integer;
     l_max_val       integer;
     l_cnt           integer;
     l_cnt_dst       integer;
     l_begin         integer;
     l_end           integer;
     l_str           varchar2(1000);
     l_tot_cnt       integer;
  pragma autonomous_transaction;
  begin
      l_tot_cnt := 0;
      l_str := ' select min(id), max(id), count(*) from test1@dst';
      execute immediate l_str into l_min_val, l_max_val, l_cnt_dst;
      dbms_output.put_line ( 'min: ' || l_min_val || ' max: ' || l_max_val
           || ' total : ' || l_cnt_dst );
      l_begin := l_min_val;
      while l_begin  < l_max_val
      loop
           begin
              l_end := l_begin + 100000;
              delete from test1@dst where id >= l_begin and id < l_end;
              l_cnt := SQL%ROWCOUNT;
              dbms_output.put_line ( 'Rows Processed : ' || l_cnt );
              l_tot_cnt := l_tot_cnt + l_cnt;
              dbms_output.put_line ( 'Rows Processed So Far : ' || l_tot_cnt );
              commit;
          exception
          when others then
               dbms_output.put_line    ( 'Error : ' || sqlcode );
     end;
 l_begin := l_begin + 100000;  
 end loop;
 dbms_output.put_line ( 'Total : ' || l_tot_cnt );
end;

**所有本地实现**

drop table test1;
create table test1
(
 id    number(38) primary key
);
insert into test1 select level from dual connect by level < 1000000;
insert into test1 values ( 1000000000000 );
insert into test1 values ( 2000000000000 );
commit;
exec dbms_stats.gather_table_stats ( ownname => user, tabname => 'test1', 
cascade => true, estimate_percent => 100 );
commit;
create or replace procedure batch_del
as
   l_min_val       integer;
   l_max_val       integer;
   l_cnt           integer;
   l_begin         integer;
   l_tot_cnt       integer;
 pragma autonomous_transaction;
 begin
  l_tot_cnt := 0;
  select min(id), max(id) into l_min_val, l_max_val from test1;
  l_begin := l_min_val;
  while l_begin  < l_max_val
  loop
    begin
     delete from test1 where id >= l_begin and id < l_begin + 10000;
     l_cnt := SQL%ROWCOUNT;
     dbms_output.put_line ( 'Rows Processed : ' || l_cnt );
     l_tot_cnt := l_tot_cnt + l_cnt;
     dbms_output.put_line ( 'Rows Processed So Far : ' || l_tot_cnt );
     commit;
   exception
     when others then
      dbms_output.put_line    ( 'Error : ' || sqlcode );
 end;
 l_begin := l_begin + 10000;
 end loop;
 dbms_output.put_line ( 'Total : ' || l_tot_cnt );
end;  

set timing on;
set serveroutput on size unli;
exec batch_del;

【问题讨论】:

  • 您是否有要删除的行?看起来不是 - 在这种情况下,为什么不截断表呢?
  • 你保留的比例是多少?将它们复制到其他地方可能会更容易,截断然后将它们复制回来;或仅包含您想要保留的 CTAS。表是否分区?索引在那个键上?你能在远程端完成工作吗?
  • 在这种情况下,您应该认真考虑使用create table x as select ... from y 创建一个只包含您想要的行的新表。然后删除旧的,重命名新的,恢复约束,索引等。删除十亿行不是一件好事。
  • 我绝对希望尝试在远程数据库上执行此操作,而不是通过 dblink,以防万一这是您的问题的原因。
  • 你不需要为此使用动态 sql - 你可以用静态 sql 做同样的事情 - 即delete from table@link where id &gt;= start_id and id &lt; start_id+1000000;从解析的角度来看,这会更好,因为动态 sql 每次调用时都需要解析,而静态 sql 则不需要(有关详细信息,请参阅Tom Kyte's answer)。

标签: sql oracle performance plsql


【解决方案1】:

这不是一个答案,但它太大而无法放入评论中,所以我们在这里!

不需要动态sql。如果我是你,我会改写为:

create or replace procedure batch_del as
  l_min_val         integer;
  l_max_val         integer;
  l_begin           integer;
  l_end             integer;
  l_rows_to_process number := 100000;
  pragma autonomous_transaction;
begin
  select min(id),
         max(id),
         count(*)
  into   l_min_val,
         l_max_val,
         l_cnt_dst
  from   test1@dst;

  l_begin := l_min_val;

  while l_begin < l_max_val
  loop
    begin
      l_end := l_begin + l_rows_to_process;

      delete from test1@dst
      where  id >= l_begin
      and    id < l_end;

      dbms_output.put_line('rows deleted: '||sql%rowcount);

      commit;
    exception
      when others then
        dbms_output.put_line('error : ' || sqlcode);
    end;

    l_begin := l_begin + l_rows_to_process;

  end loop;
end;
/

或者,如果您有非连续的 id,也许这对您来说会更高效:

create or replace procedure batch_del as
  type type_id_array is table of number index by pls_integer;
  l_min_id_array     type_id_array;
  l_max_id_array     type_id_array;
  l_rows_to_process  number := 10000;
  pragma autonomous_transaction;
begin

  select min(id) min_id,
         max(id) max_id bulk collect
  into   l_min_id_array,
         l_max_id_array
  from   (select --/*+ driving_site(t1) */
                 id,
                 ceil((row_number() over(order by id)) / l_rows_to_process) grp
          from   test1 t1)
  group  by grp
  order  by grp;

  for i in l_min_id_array.first..l_min_id_array.last
  loop
    begin
      delete from test1
      where  id between l_min_id_array(i) and l_max_id_array(i);

      dbms_output.put_line('rows deleted in loop '||i||': '||sql%rowcount);

      commit;
    exception
      when others then
        -- i hope there is some better way of logging an error in your
        -- production db; e.g. a separate procedure writing to a log table.
        dbms_output.put_line('error in loop '||i||': ' || sqlcode);
    end;
  end loop;
end batch_del;
/

【讨论】:

  • @krokodilko - 我删除了 DBMS_OUTPUT 语句,这大大降低了 PGA 的使用率。要回答您之前的问题 - 这是 Oracle 11.2.0.4 标准版,消息在大约 6000 万次迭代后开始出现,相当于 1.5 Gb(每行 DBMS_OUTPUT 60,000,000 * 25 个字节)。
  • 我会尽快在非连续 ID 上尝试您的解决方案并发布结果。
  • 酌情删除 dbms_outputs
  • 我使用了非连续 ID 的解决方案,它有效!!。由于在这种情况下循环的数量减少了很多 - DBMS_OUTPUT 溢出输出缓冲区的机会也减少了。但这一点很好 - 没有理由拥有数百万行日志记录。
  • 如何将多个答案标记为解决了我的问题的答案?
【解决方案2】:

您在程序中使用 DMS_Output:

dbms_output.put_line ( 'Rows Processed : ' || l_cnt );
....
dbms_output.put_line ( 'Rows Processed So Far : ' || l_tot_cnt );

上述每个调用都会生成一个大约 25 个字符长(约 25 个字节)的字符串。

PUT_LINE 过程不会在控制台上打印“在线”消息,而是将所有消息放入内存缓冲区,请参阅文档中的说明:DBMS_OUTPUT

注意:
使用 DBMS_OUTPUT 发送的消息直到 发送子程序或触发器完成。 没有机制 在执行过程期间刷新输出。
....
....
规则和限制
最大行大小为 32767 字节。

默认缓冲区大小为 20000 字节。最小大小为 2000 字节,最大无限制。

您在问题中写道:

大约需要 50,000,000,000 次迭代

很容易估计存储 DBMS_Output 消息所需的内存大小,
只需:2 条消息,每条 25 字节,50,000,000,000 次迭代

2 * 25 * 50,000,000,000 = 2 500 000 000 000 bytes

您似乎需要大约 2500 GB(~2.5 TB)的内存来存储来自您的程序的所有消息。 PGA_AGGREGATE_TARGET = 1.5 gB 肯定太低了。


只需从您的代码中删除 DBMS_Output,没有人(任何人)能够从控制台读取 50~1000 亿条消息。
如果你想监控这个过程,使用DBMS_APPLICATION_INFO.SET_CLIENT_INFO Procedure,你可以存储最多64个字符的消息,然后查询V$SESSION视图来检索最后一条消息。

【讨论】:

  • 哇,没想到脚本中使用了 dbms_output,但我看到了更新后的帖子。绝对不希望在生产代码中使用它,尤其是这样的循环。接得好! (我会通过自治过程记录提交指向日志表)
  • select min(id) min_id, max(id) max_id bulk collect into l_min_id_array, l_max_id_array from (select --/*+driving_site(t1) */ id, ceil((row_number() over( order by id)) / l_rows_to_process) grp from test1 t1) group by grp order by grp;当我在上面尝试作为 EXECUTE IMMEDIATE 的一部分时,我得到 ORA-03001 Unimplemented feature
【解决方案3】:

为了让@boneist 回答更灵活,可以使用 EXECUTE IMMEDIATE,如下所示:

loop
.....
  str := 'select min(id) min_id, max(id) max_id l_min_id_array, 
           l_max_id_array from (select id, ceil((row_number() over(order by 
           id)) / l_rows_to_process) grp from test1 t1) group by grp order 
           by grp';

  execute immediate str bulk collect into l_min_id_array, l_max_id_array;
  ....
 end loop;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-05
    • 2012-12-24
    • 1970-01-01
    • 2021-12-01
    • 2013-08-10
    • 2017-06-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多