总结
我认为更新到 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 会运行得更快,但其他所有更新都会运行得至少稍微慢一些。