【问题标题】:update x set y = null takes a long timeupdate x set y = null 需要很长时间
【发布时间】:2011-12-26 02:44:00
【问题描述】:

在工作中,我有一张大桌子(大约 300 万行,比如 40-50 列)。我有时需要清空一些列并用新数据填充它们。让我没想到的是

UPDATE table1 SET y = null

比用生成的数据填充列要花费更多的时间,例如,在 sql 查询中从同一表的其他列或从子查询中的其他表查询。如果我一次遍历所有表行(如上面的更新查询)或者我是否使用游标逐行遍历表(使用 pk),这并不重要。不管我是在工作中使用大表还是创建一个小测试表并用几十万个测试行填充它。将列设置为 null 总是比使用一些动态数据(每行不同)更新列需要更长的时间(在整个测试过程中,我遇到了 2 到 10 个因子)。

这是什么原因?将列设置为空时,Oracle 会做什么?或者 - 我的推理错误是什么?

感谢您的帮助!

P.S.:我使用的是 oracle 11g2,使用 plsql developer 和 oracle sql developer 找到了这些结果。

【问题讨论】:

  • 你能发布你的执行/解释计划吗?
  • 如果我一次遍历整个表,则没有 where 子句。如果我逐行浏览表,那么会有一个 where 子句引用表的主键。结果在两个版本中保持不变。至于执行计划,今天晚些时候我会准备一个一步一步的例子来重现结果。
  • 不知道重建表是否更快?我的意思是create table newtab select col1, col2, cast(null as something), col4 from oldtab
  • 我相信重新创建会更快。
  • 索引?域索引?约束?触发?

标签: sql oracle plsql oracle11g


【解决方案1】:

我会尝试 Tom Kyte 对大型更新的建议。 当涉及到大表时,最好是这样:取几行,更新它们,再多取一些,更新那些等等。不要试图对所有表发出更新。这从一开始就是一个杀手锏。

基本上是创建binary_integer索引表,一次取10行,然后更新。

这是我成功用于大型表的一段代码。因为我很懒,现在就像凌晨 2 点一样,我只是复制粘贴到这里让你弄清楚,但如果你需要帮助,请告诉我:

DECLARE

   TYPE BookingRecord IS RECORD ( 
      bprice  number,
      bevent_id number,
      book_id number
      );

   TYPE array is TABLE of BookingRecord index by binary_integer;
  l_data array;

 CURSOR c1 is
    SELECT LVC_USD_PRICE_V2(ev.activity_version_id,ev.course_start_date,t.local_update_date,ev.currency,nvl(t.delegate_country,ev.sponsor_org_country),ev.price,ev.currency,t.ota_status,ev.location_type) x,
       ev.title,
       t.ota_booking_id
      FROM ota_gsi_delegate_bookings_t@diseulprod t,
           inted_parted_events_t@diseulprod ev
      WHERE t.event_id = ev.event_id
        and t.ota_booking_id = 
BEGIN
   open c1;
        loop
            fetch c1 bulk collect into l_data limit 20;

             for i in 1..l_data.count
               loop
                   update ou_inc_int_t_01 
                      set price = l_data(i).bprice,
                          updated = 'Y'
                    where booking_id = l_data(i).book_id;
               end loop;

           exit when c1%notfound;
       end loop;
       close c1;
END;

【讨论】:

  • 我强烈反对用许多小 SQL 语句替换一个大 SQL 语句会更好。 (尽管在多用户环境中,您可能偶尔需要为并发执行此操作,或者因为资源有限,例如小型 UNDO 表空间。)多个小型 SQL 语句需要时间在 SQL 和 PL/SQL 之间切换。与多个 UPDATE 相比,单个 UPDATE 可能需要更少的 UNDO。 (A FORALL 而不是 FOR 将有助于上下文切换,但似乎根本不会减少 UNDO 大小。)
【解决方案2】:

总结

我认为更新到 null 比较慢,因为 Oracle(错误地)试图利用它存储 null 的方式,导致它频繁地重新组织块中的行(“堆块压缩”),从而创建了很多额外的 UNDO 和 REDO。

null 有什么特别之处?

来自Oracle Database Concepts

“如果空值位于具有数据值的列之间,则将空值存储在数据库中。在这些情况下,它们需要 1 个字节来存储列的长度(零)。

一行中的尾随空值不需要存储,因为新的行标题表明前一行中的剩余列为空。例如,如果表的最后三列为空,则不会为这些列存储任何信息。在有很多列的表中, 应该最后定义更有可能包含空值的列以节省磁盘空间。”

测试

基准更新非常困难,因为仅从更新语句无法衡量更新的真实成本。例如,日志开关将 并非每次更新都会发生,延迟块清除将在以后发生。为了准确地测试更新,应该有多次运行, 每次运行都应该重新创建对象,并且应该丢弃高值和低值。

为简单起见,下面的脚本不会抛出高低结果,并且只测试具有单列的表。但是不管列的数量、它们的数据以及更新了哪一列,问题仍然存在。

我使用来自 http://www.oracle-developer.net/utilities.php 的 RunStats 实用程序来比较更新为一个值和更新为空值的资源消耗。

create table test1(col1 number);

BEGIN
    dbms_output.enable(1000000);

   runstats_pkg.rs_start;

    for i in 1 .. 10 loop
        execute immediate 'drop table test1 purge';
        execute immediate 'create table test1 (col1 number)';
        execute immediate 'insert /*+ append */ into test1 select 1 col1
            from dual connect by level <= 100000';
        commit;
        execute immediate 'update test1 set col1 = 1';
        commit;
    end loop;

   runstats_pkg.rs_pause;
   runstats_pkg.rs_resume;

    for i in 1 .. 10 loop
        execute immediate 'drop table test1 purge';
        execute immediate 'create table test1 (col1 number)';
        execute immediate 'insert /*+ append */ into test1 select 1 col1
            from dual connect by level <= 100000';
        commit;
        execute immediate 'update test1 set col1 = null';
        commit;
    end loop;

   runstats_pkg.rs_stop();
END;
/

结果

有很多不同之处,这是我认为最相关的四个:

Type  Name                                 Run1         Run2         Diff
----- ---------------------------- ------------ ------------ ------------
TIMER elapsed time (hsecs)                1,269        4,738        3,469
STAT  heap block compress                     1        2,028        2,027
STAT  undo change vector size        55,855,008  181,387,456  125,532,448
STAT  redo size                     133,260,596  581,641,084  448,380,488

解决方案?

我能想到的唯一可能的解决方案是启用表压缩。压缩表不会发生尾随空存储技巧。 因此,即使 Run2 的“堆块压缩”数字变得更高,从 2028 到 23208,我想它实际上并没有做任何事情。 两次运行之间的重做、撤消和经过的时间几乎与启用表压缩的情况相同。

但是,表压缩有很多潜在的缺点。更新为 null 会运行得更快,但其他所有更新都会运行得至少稍微慢一些。

【讨论】:

    【解决方案3】:

    那是因为它删除阻止该数据。

    delete 是最难的操作。 如果你可以避免delete,那就去做吧。

    我建议您创建另一个表,该列为空(例如Create table as select,或insert select),并用您的过程填充它(列)。删除旧表,然后用当前名称重命名新表。

    更新:

    另一件重要的事情是您应该使用新值按原样更新列。将它们设置为 null 然后重新填充它们是没有用的。 如果您没有所有行的值,则可以像这样进行更新:

    udpate table1 
    set y = (select new_value from source where source.key = table1.key)
    

    并将源中不存在的那些行设置为空。

    【讨论】:

      【解决方案4】:

      Y 列是否已编入索引?可能将列设置为 null 意味着 Oracle 必须从索引中删除,而不仅仅是更新它。如果是这种情况,您可以在更新数据后删除并重建它。

      编辑:

      只是 Y 列出现问题,还是独立于正在更新的列?你能发布表定义,包括约束吗?

      【讨论】:

      • 抱歉,忘了说,该列没有索引。
      【解决方案5】:

      还可以帮助加速更新的是使用alter table table1 nologging,这样更新就不会生成重做日志。另一种可能性是删除该列并重新添加它。因为这是一个 DDL 操作,所以它既不会产生重做也不会产生撤销。

      【讨论】:

      • 删除列会做同样的事情。从块中删除数据。在性能方面是相同的 - 相同的持续时间。
      • 真正的弗罗林,我忽略了这一点。 nologging 提示仍然有效。谢谢指正!
      猜你喜欢
      • 2017-06-20
      • 1970-01-01
      • 2014-09-26
      • 1970-01-01
      • 2016-12-03
      • 1970-01-01
      • 2013-09-07
      • 2020-08-26
      • 2014-10-09
      相关资源
      最近更新 更多