再上一篇:9.2.1 redo 和undo如何协作?
上一篇:9.3提交和回滚处理
主页
下一篇:9.5分析 undo
再下一篇:9.6小结
文章列表

9.4分析 redo

Oracle 9i 10g编程艺术:深入数据库体系结构

作为一名开发人员,应该能够测量你的操作生成了多少redo,这往往很重要。生成的 redo越多,你 的操作花费的时间就越长,整个系统也会越慢。你不光在影响你自己的会话,还会影响每一个会话。redo 管理是数据库中的一个串行点。任何Oracle实例都只有一个LGWR,最终所有事务都会归于LGWR,要求这 个进程管理它们的redo,并BOMMIT其事务,LGWR要做的越多,系统就会越慢。通过查看一个操作会生成 多少redo,并对一个问题的多种解决方法进行测试,可以从中找出最佳的方法。

9.4.1测量 redo

要查看生成的redo量相当简单,这在本章前面已经见过。我使用了 SQL*Plus的内置特性AUTOTRACE。 不过AUTOTRACE只能用于简单的DML,对其他操作就力所不能及了,例如,它无法查看一个存储过程调用 做了什么。为此,我们需要访问两个动态性能视图:
V$MYSTAT,其中有会话的提交信息。
V$STATNAME,这个视图能告诉我们V$MYSTAT中的每一行表示什么(所查看的统计名)。 因为我经常要做这种测量,所以使用了两个脚本,分别为mystat和mystat2。mystat.sql脚本把我

感兴趣的统计初始值(如redo大小)保存在一个SQL*Plus变量中:
set verify off
column value new_val V
define S="&1"
set autotrace off
select a.name, b.value
from v$statname a, v$mystat b where a.statistic# = b.statistic#
and lower(a.name) like '%' || lower('&S')||'%'
/

mystat2.sql脚本只是打印出该统计的初始值和结束值之差:
set verify off
select a.name, b.value V, to_char(b.value-&V,'999,999,999,999') diff from v$statname a, v$mystat b
where a.statistic# = b.statistic#
and lower(a.name) like '%' || lower('&S')||'%'
/

下面,可以测量一个给定事务会生成多少redo。我们只需这样做:
@mystat "redo size"
...process...
@mystat2


例如:
ops$tkyte@ORA10G> @mystat "redo size"
NAME VALUE
------------------------------ ---------- redo size 496
ops$tkyte@ORA10G> insert into t select * from big_table;

100000 rows created.
ops$tkyte@ORA10G> @mystat2
NAME V DIFF
------------------------------ ---------- --------------
--
redo size 37678732 37,678,236
如上所示,这个INSERT生成了大约37MB的redo。你可能想与一个直接路径INSERT生成的redo做
个比较,如下:
注意 这一节的例子在一个NOARCHIVELOG模式的数据库上执行。如果你的数据库采用ARCHIVELOG模 式,要想观察到这种显著的差异,必须把表置为NOLOGGING。稍后的“SQL中设置NOLOGGING”一 节中会更详细地分析 NOLOGGING 属性。不过,在一个“实际”的系统上,对于所有“非日志”

(nonlogged)操作,一定要与你的DBA协调好。
ops$tkyte@ORA10G> @mystat "redo size"
NAME VALUE
------------------------------ ---------- redo size 37678732
ops$tkyte@ORA10G> insert /*+ APPEND */ into t select * from big_table;
100000 rows created.
ops$tkyte@ORA10G> @mystat2 ops$tkyte@ORA10G> set echo off
NAME V DIFF
------------------------------ ---------- --------------
--
redo size 37714328 35,596
以上方法使用了 V$MYSTAT视图,这个方法对于查看各个选项的副作用通常很有用。mystat.sql
脚本适用于有一两个操作的小测试,但是如果我们想完成很大的一系列测试呢?在此可以用到一个很小 的测试工具。在下一节中,我们将建立和使用这个测试工具,并利用一个表来记录我们的结果,从而分析 BEFORE触发器生成的redo。

9.4.2redo 生成和BEFORE/AFTER触发器

经常有人问我:“除了可以在BEFORE触发器中修改一行的值外,BEFORE和AFTER触发器之间还有没 有其他的区别?”嗯,对于这个问题,答案是当然有。BEFORE触发器要额外的redo信息,即使它根本没 有修改行中的任何值。实际上,这是一个很有意思的案例研究,使用上一节介绍的技术,我们会发现:
BEFORE或AFTER触发器不影响DELETE生成的redo。
在Oracle9i Release 2 及以前版本中,BEFORE或AFTER触发器会使INSERT生成同样数量 的额外redo。在Oracle 10g中,则不会生成任何额外的redo。
在Oracle9i Release 2及以前的所有版本中,UPDATE生成的redo只受BEFORE触发器的影 响。AFTER触发器不会增加任何额外的redo。不过,在Oracle 10g中,情况又有所变化。具体 表现为:
总的来讲,如果一个表没有触发器,对其更新期间生成的redo量总是比 Oracle9i 及以前版本中要少。看来这是Oracle着力解决的一个关键问题:对于触发器的表,要减 少这种表更新所生成的redo量。
在Oracle 10g中,如果表有一个BEFORE触发器,则其更新期间生成的redo量比9i
中更大。

如果表有AFTER触发器,则更新所生成的redo量与9i中一样。 为了完成这个测试,我们要使用一个表T,定义如下:
create table t ( x int, y char(N), z date );
但是,创建时N的大小是可变的。在这个例子中,将使用N=30、100、500、1,000和2,000来得到不
同宽度的行。针对不同大小的Y列运行测试,再来分析结果。我使用了一个很小的日志表来保存多次运行 的结果:
create table log ( what varchar2(15), -- will be no trigger, after or before
op varchar2(10), -- will be insert/update or delete rowsize int, -- will be the size of Y
redo_size int, -- will be the redo generated
rowcnt int ) -- will be the count of rows affected

这里使用以下DO_WORK存储过程来生成事务,并记录所生成的redo。子过程REPORT是一个本地过程
(只在DO_WORK过程中可见),它只是在屏幕上报告发生了什么,并把结果保存到LOG表中:

ops$tkyte@ORA10G> create or replace procedure do_work( p_what in varchar2 )
2 as
3 l_redo_size number;
4 l_cnt number := 200;
5
6 procedure report( l_op in varchar2 )
7 is
8 begin
9 select v$mystat.value-l_redo_size
10 into l_redo_size
11 from v$mystat, v$statname
12 where v$mystat.statistic# = v$statname.statistic#
13 and v$statname.name = 'redo size';
14
15 dbms_output.put_line(l_op || ' redo size = ' || l_redo_size ||
16 ' rows = ' || l_cnt || ' ' ||
17 o_char(l_redo_size/l_cnt,'99,999.9') ||
18 ' bytes/row' );
19 insert into log
20 select p_what, l_op, data_length, l_redo_size, l_cnt
21 from user_tab_columns
22 where table_name = 'T'
23 and column_name = 'Y';
24 end;
本地过程SET_REDO_SET会查询V$MYSTAT和V$STATNAME,来获取到目前为止会话已生成的当前redo
量。它将过程中的变量L_REDO_SIZE设置为这个值:
25 procedure set_redo_size
26 as
27 begin
28 select v$mystat.value
29 into l_redo_size
30 from v$mystat, v$statname
31 where v$mystat.statistic# = v$statname.statistic#
32 and v$statname.name = 'redo size';
33 end;

接下来是主例程。它收集当前的 redo大小,运行一个INSERT/UPDATE/DELETE,然后把该操作生成的

redo保存到LOG表中:
34 begin
35 set_redo_size;
36 insert into t
37 select object_id, object_name, created
38 from all_objects
39 where rownum <= l_cnt;
40 l_cnt := sql%rowcount;
41 commit;
42 report('insert');
43
44 set_redo_size;
45 update t set y=lower(y);

46 l_cnt := sql%rowcount;
47 commit;
48 report('update');
49
50 set_redo_size;
51 delete from t;
52 l_cnt := sql%rowcount;
53 commit;
54 report('delete');
55 end;
56 /
一旦有了这个例程,下面将 Y列的宽度设置为2,000,然后运行以下脚本来测试3 种场景:没有触发

器、有BEFORE触发器,以及有AFTER触发器。
ops$tkyte@ORA10G> exec do_work('no trigger');
insert redo size = 505960 rows = 200 2,529.8 bytes/row update redo size = 837744 rows = 200 4,188.7 bytes/row delete redo size = 474164 rows = 200 2,370.8 bytes/row PL/SQL procedure successfully completed.
ops$tkyte@ORA10G> create or replace trigger before_insert_update_delete
2 before insert or update or delete on T for each row
3 begin
4 null;
5 end;
6 /

Trigger created.
ops$tkyte@ORA10G> truncate table t; Table truncated.
ops$tkyte@ORA10G> exec do_work('before trigger'); insert redo size = 506096 rows = 200 2,530.5 bytes/row update redo size = 897768 rows = 200 4,488.8 bytes/row delete redo size = 474216 rows = 200 2,371.1 bytes/row PL/SQL procedure successfully completed.
ops$tkyte@ORA10G> drop trigger before_insert_update_delete; Trigger dropped.
ops$tkyte@ORA10G> create or replace trigger after_insert_update_delete
2 after insert or update or delete on T
3 for each row
4 begin
5 null;
6 end;
7 /
Trigger created.
ops$tkyte@ORA10G> truncate table t; Table truncated.

ops$tkyte@ORA10G> exec do_work( 'after trigger' );
insert redo size = 505972 rows = 200 2,529.9 bytes/row update redo size = 856636 rows = 200 4,283.2 bytes/row delete redo size = 474176 rows = 200 2,370.9 bytes/row
PL/SQL procedure successfully completed.
前面的输出是在把 Y 大小设置为 2,000字节时运行脚本所得到的。完成所有运行后,就能查询LOG

表,并看到以下结果:
ops$tkyte@ORA10G> break on op skip 1
ops$tkyte@ORA10G> set numformat 999,999 ops$tkyte@ORA10G> select op, rowsize, no_trig,
before_trig-no_trig, after_trig-no_trig
2 from
3 ( select op, rowsize,
4 sum(decode( what, 'no trigger', redo_size/rowcnt,0 ) ) no_trig,
5 sum(decode( what, 'before trigger', redo_size/rowcnt, 0 ) ) before_trig,
6 sum(decode( what, 'after trigger', redo_size/rowcnt, 0 ) ) after_trig
7 from log
8 group by op, rowsize
9 )
10 order by op, rowsize
11 /
OP ROWSIZE NO_TRIG BEFORE_TRIG-NO_TRIG AFTER_TRIG-NO_TRIG

----------

--------------

---------------- -------------------

-----------------

---------------------------------


delete 30 291
0 0
100 364
-1 -0
500 785
-0 0
1,000 1,307
-0 -0
2,000 2,371
0 -0
insert 30 296
0 -0
100 367
0 0
500 822
1 1
1,000 1,381
-0 -0
2,000 2,530
0 0
update 30 147
358 152
100 288
363 157
500 1,103
355 150
1,000 2,125
342 137
2,000 4,188
300 94
15 rows selected.
现在,我想知道日志模式(ARCHIVELOG和NOARCHIVELOG模式)是否会影响这些结果。我发现答案是 否定的,这两种模式得到的结果数一样。我很奇怪为什么这个结果与Expert One-on-One Oracle第1版中 的结果有很大的差异。你现在读的这本书或多或少就是以那本书为基础的。出版那本书时,Oracle的最新 版本是Oracle8i8.1.7。前面所示的 Oracle 10g结果与Oracle8i得到的结果大不相同,但是对于Oracle9i, 这个表中所示的结果则与Oracle8i的结果很接近:
我发现,在Oracle9i Release 2和Oracle 10g这两个版本之间,触发器对事务实际生成的redo存 在不同的影响。可以很容易地看到这些执行:
是否存在触发器对DELETE没有影响(DELETE还是不受触发器的影响)。
在Oracle9i Release 2及以前版本中,INSERT会受到触发器的影响。初看上去,你可能会 说,Oracle 10g优化了INSERT,所以它不会受到影响,但是再看看Oracle 10g中无触发器时 生成的redo总量,你会看到,这与Oracle9i Release 2及以前版本中有触发器时生成的redo 量是一样的。所以,并不是Oracle 10g减少了有触发器时INSERT生成的redo量,而是所生成 的redo量是常量(有无触发器都会生成同样多的redo),无触发器时,Oracle 10g中的INSERT 比Oracle9i中生成的redo要多。
在9i中,UPDATE会受BEFORE触发器的影响,但不受AFTER触发器的影响。初看上去,似 乎Oracle 10g中改成了两个触发器都会影响UPDATE。但是通过进一步的分析,可以看到,实际 上 Oracle 10g中无触发器是 UPDATE生成的 redo有所下降,下降的量正是有触发器时 UPDATE 生成的redo量。所用与9i和10g中INSERT的情况恰恰相反,与9i相比,没有触发器时Oracle 10g 中UPDATE生成的redo量会下降。
表9-3对此做了一个总结,这里列出了Oracle9i及以前版本与Oracle 10g中触发器的DML操作生成 的redo量分别有怎样的影响。
表9-3 触发器对redo生成的影响
DML操作 AFTER触发器 BEFORE触发器 AFTER触发器 BEFORE触发器
(10g以前) (10g以前) (10g) (10g) DELETE 不影响 不影响 不影响
不影响
INSERT 增加redo 增加redo 常量redo 常量redo
UPDATE 增加 redo 不影响 增加 redo 增加

redo
测试用例的重要性
更新这本书的第一版时,我切切实实地感受到,这是一个绝好的例子,可以充分说明为什么要用测试
用例展示一件事物到底是好是坏。如果我在第一版中只是下了个结论:“触发器会如此这般影响INSERT、 UPDATE和DELETE”,而没有提供一种方法来加以度量,另外倘若在这里也没有提供测试用例,那我很有可 能还会沿袭同样的结论。在 Oracle9i和Oracle 10g 中同样地运行这些测试用例,却会得到不同的结果, 所以现在我能很容易地展示出这两个版本间的差异,并且知道Oracle数据库确实“发生了变化”。在对第

一版更新的过程中,我一次次地发现,如果没有这些测试用例,我可能完全依据过去的经验妄下断言,得 出许多错误的结论。
现在你应该知道怎么来估计redo量,这是每一个开发人员应该具备的能力。你可以:
估计你的“事务”大小(你要修改多少数据)。
在要修改的数据量基础上再加10%~20%的开销,具体增加多大的开销取决于你要修改的行 数。修改行越多,增加的开销就越小。
对于UPDATE,要把这个估计值加倍。
在大多数情况下,这将是一个很好的估计。UPDATE的估计值加倍只是一个猜测,实际上这取决于你 修改了多少数据。之所以加倍,是因为在此假设要取一个X字节的行,并把它更新(UPDATE)为另一个X 字节的行。如果你取一个小行(数据量较少的行),要把它更新为一个大行(数据量较多的行),就不用对 这个值加倍(这更像是一个INSERT)。如果取一个大行,而把它更新为一个小行,也不用对这个值加倍(这 更像是一个DELETE)。加倍只是一种“最坏情况”,因为许多选项和特性会对此产生影响,例如,存在索引 或者没有索引(我这里就没有索引)也会影响这个底线。维护索引结构所必须的工作量对不同的UPDATE来 说是不同的,此外还有一些影响因素。除了前面所述的固定开销外,还必须把触发器的副作用考虑在内。 另外要考虑到Oracle为你执行的隐式操作(如外键上的ON DELETE CASCADE设置)。有了这些考虑,你就 能适当地估计redo量以便调整事务大小以及实现性能优化。不过,只有通过实际的测试才能得到确定的答 案。给定以上脚本,你应该已经知道如何对任何对象和事务自行测量redo。

9.4.3我能关掉重做日志生成吗?

这个问题经常被问到。答案很简单:不能。因为重做日志对于数据库至关重要;它不是开销,不是浪 费。不论你是否相信,重做日志对你来说确确实实必不可少。这是无法改变的事实,也是数据库采用的工 作方式。如果你真的“关闭了redo”,那么磁盘驱动器的任何暂时失败、掉电或每个软件崩溃都会导致整 个数据库不可用,而且不可恢复。不过需要指出,有些情况下执行某些操作时确实可以不生成重做日志。
注意 对于Oracle9i Release 2,DBA可能把数据库置于FORCE LOGGING模式。在这种情况下,所有 操作都会计入日志。查询 SELECT FORCE_LOGGING FROM V$DATABASE可以查看是否强制为日志模 式。这个特性是为了支持Data Guard, Data Guard是Oracle的一个灾难恢复特性,它依赖于redo 来维护一个备用数据库(standby database)备份。

1. 在 SQL 中设置NOLOGGING


有些SQL语句和操作支持使用NOLOGGING子句。这并不是说:这个对象的所有操作在执行时都不生成 重做日志,而是说有些特定操作生成的redo会比平常(即不使用NOLOGGING子句时)少得多。注意,我只 是说“redo”少得多,而不是“完全没有redo“。所有操作都会生成一些redo——不论日志模式是什么, 所有数据字典操作都会计入日志。只不过使用NOLOGGING子句后,生成的redo量可能会显著减少。下面是 使用NOLOGGING子句的一个例子。为此先在采用ARCHIVELOG模式运行的一个数据库中运行以下命令:
ops$tkyte@ORA10G> select log_mode from v$database;
LOG_MODE

------------ ARCHIVELOG
ops$tkyte@ORA10G> @mystat "redo size" ops$tkyte@ORA10G> set echo off
NAME VALUE
---------- ---------- redo size 5846068
ops$tkyte@ORA10G> create table t
2 as
3 select * from all_objects; Table created.
ops$tkyte@ORA10G> @mystat2 ops$tkyte@ORA10G> set echo off
NAME V DIFF
------------- ------------- ---------------- redo size 11454472 5,608,404

这个CREATE TABLE生成了大约5.5MB的redo信息。接下来删除这个表,再重建,不过这一次采用 NOLOGGING模式:
ops$tkyte@ORA10G> drop table t;
Table dropped.

ops$tkyte@ORA10G> @mystat "redo size" ops$tkyte@ORA10G> set echo off
NAME VALUE
------------ ------------- redo size 11459508
ops$tkyte@ORA10G> create table t
2 NOLOGGING
3 as
4 select * from all_objects; Table created.
ops$tkyte@ORA10G> @mystat2 ops$tkyte@ORA10G> set echo off
NAME V DIFF
---------- ---------- ---------------- redo size 11540676 81,168
这一次,只生成了80KB的redo信息。
可以看到,差距很悬殊:原来有5.5MB的redo,现在只有80KB。5.5MB 是实际的表数据本身;现在 它直接写至磁盘,对此没有生成重做日志。
如果对一个NOARCHIVELOG模式的数据库运行这个测试,就看不到什么差别。在NOARCHIVELOG模式的 数据库中,除了数据字典的修改外,CREATE TABLE 不会记录日志。如果你想在NOARCHIVELOG模式的数据 库上看到差别,可以把对表T 的DROP TABLE 和CREATE TABLE换成 DROP INDEX和 CREATE INDEX。默认情 况下,不论数据库以何种模式运行,这些操作都会生成日志。从这个例子可以得出一个很有意义的提示: 要按生产环境中所采用的模式来测试你的系统,因为不同的模式可能导致不同的行为。你的生产系统可能
采用AUCHIVELOG模式运行;倘若你执行的大量操作在ARCHIVELOG模式下会生成redo,而在NOARCHIVELOG 模式下不会生成redo,你肯定想在测试时就发现这一点,而不要等到系统交付给用户时才暴露出来!
这么说,好像所有工作都应该尽可能采用 NOLOGGING模式,是这样吗?实际上,答案很干脆:恰恰相 反。必须非常谨慎地使用这种模式,而且要与负责备份和恢复的人沟通之后才能使用。下面假设你创建了 一个非日志模式的表,并作为应用的一部分(例如,升级脚本中使用了 CREATE TABLE AS SELECT NOLOGGING)。用户白天修改了这个表。那天晚上,表所在的磁盘出了故障。“没关系“,DBA说”数据库在 用ARCHIVELOG模式运行,我们可以执行介质恢复“。不过问题是,现在无法从归档重做日志恢复最初创建 的表,因为根本没有生成日志。这个表将无法恢复。由此可以看出使用NOLOGGIG操作最重要的一点是:必 须与DBA和整个系统协调。如果你使用了NOLOGGING操作,而其他人不知道这一点,你可能就会拖DBA的 后退,使得出现介质失败后DBA无法全面地恢复数据库。必须谨慎而且小心地使用这些NOLOGGING操作。
关于NOLOGGING操作,需要注意以下几点:
事实上,还是会生成一定数量的redo。这些redo的作用是保护数据字典。这是不可避免的 。 与以前(不使用NOLOGGING)相比,尽管生成的redo量要少多了,但是确实会有一些redo。
NOLOGGING不能避免所有后续操作生成redo。在前面的例子中,我创建的并非不生成日志 的表。只是创建表(CREATE TABLE)这一个操作没有生成日志。所有后续的“正常“操作(如 INSERT、UPDATE和DELETE)还是会生成日志。其他特殊的操作(如使用SQL*Loader的直接路 径加载,或使用INSERT /*+ APPEND */语法的直接路径插入)不生成日志(除非你ALTER这个 表,再次启用完全的日志模式)。不过,一般来说,应用对这个表执行的操作都会生成日志。
在一个ARCHIVELOG模式的数据库上执行NOLOGGING操作后,必须尽快为受影响的数据文件 建立一个新的基准备份,从而避免由于介质失败而丢失对这些对象的后续修改。实际上,我们 并不会丢失后来做出的修改,因为这些修改确实在重做日志中;我们真正丢失的只是要应用这 些修改的数据(即最初的数据)。

2. 在索引上设置NOLOGGING


使用NOLOGGING选项有两种方法。你已经看到了前一种,也就是把 NOLOGGING关键字潜在SQL命令中 。 另一种方法是在段(索引或表)上设置NOLOGGING属性,从而隐式地采用NOLOGGING模式来执行操作。例 如,可以把一个索引或表修改为默认采用NOLOGGING模式。这说明,以后重建这个索引不会生成日志(其 他索引和表本身可能还会生成redo,但是这个索引不会):
ops$tkyte@ORA10G> create index t_idx on t(object_name);
Index created.
ops$tkyte@ORA10G> @mystat "redo size" ops$tkyte@ORA10G> set echo off
NAME VALUE

---------- ----------
redo size 13567908
ops$tkyte@ORA10G> alter index t_idx rebuild;
Index altered. ops$tkyte@ORA10G> @mystat2 ops$tkyte@ORA10G> set echo off
NAME V DIFF

------------- -------------- ---------------- redo size 15603436 2,035,528

这个索引采用LOGGING模式(默认),重建这个索引会生成2MB的重做日志。不过,可以如下修改这 个索引:
ops$tkyte@ORA10G> alter index t_idx nologging;
Index altered.
ops$tkyte@ORA10G> @mystat "redo size" ops$tkyte@ORA10G> set echo off
NAME VALUE
---------- ---------- redo size 15605792
ops$tkyte@ORA10G> alter index t_idx rebuild; Index altered.

ops$tkyte@ORA10G> @mystat2 ops$tkyte@ORA10G> set echo off
NAME V DIFF
------------- ------------- ---------------- redo size 15668084 62,292
现在它只生成61KB的redo。但是,现在这个索引没有得到保护(unprotected),如果它所在的数据 文件失败而必须从一个备份恢复,我们就会丢失这个索引数据。了解这一点很重要。现在索引是不可恢复 的,所以需要做一个备份。或者,DBA也可以干脆创建索引,因为完全可以从表数据直接创建索引。

3. NOLOGGING 小结

可以采用NOLOGGING模式执行以下操作: 索引的创建和ALTER(重建)。
表的批量INSERT(通过/*+APPEND */提示使用“直接路径插入“。或采用SQL*Loader直接 路径加载)。表数据不生成 redo,但是所有索引修改会生成redo,但是所有索引修改会生成redo
(尽管表不生成日志,但这个表上的索引却会生成redo!)。 LOB操作(对大对象的更新不必生成日志)。
通过CREATE TABLE AS SELECT创建表。 各种ALTER TABLE操作,如MOVE和SPLIT。
在一个ARCHIVELOG模式的数据库上,如果NOLOGGING使用得当,可以加快许多操作的速度,因为它 能显著减少生成的重做日志量。假设你有一个表,需要从一个表空间移到另一个表空间。可以适当地调度 这个操作,让它在备份之后紧接着发生,这样就能把表ALTER 为NOLOGGING模式,移到表,创建索引(也 不生成日志),然后再把表ALTER回LOGGING模式。现在,原先需要X小时才能完成的操作可能只需要X/2 小时(运行是会不会真的减少50%的时间,这一点我不敢打保票!)。要想适当地使用这个特性,需要DBA 的参与,或者必须与负责数据库备份和恢复(或任何备用数据库)的人沟通。如果这个人不知道使用了这 个特性,一旦出现介质失败,就可能丢失数据,或者备用数据库的完整性可能遭到破坏。对此一定要三思。

9.4.4为什么不能分配一个新日志?


老是有人问我这个问题。这样做会得到一条警告消息(可以在服务器上的alert.log中看到):
Thread 1 cannot allocate new log, sequence 1466
Checkpoint not complete
Current log# 3 seq# 1465 mem# 0: /home/ora10g/oradata/ora10g/redo03.log
警告消息中也可能指出Archival required而不是Checkpoint not complete,但是效果几乎都一样 。 DBA必须当心这种情况。如果数据库试图重用一个在线重做日志文件,但是发现做不到,就会把这样一条 消息写到服务器上的alert.log中。如果 DBWR还没有完成重做日志所保护数据的检查点(checkpointing), 或者ARCH还没有把重做日志文件复制到归档目标,就会发生这种情况。对最终用户来说,这个时间点上数 据库实际上停止了。它会原地不动。DBWR或ARCH将得到最大的优先级以将redo块刷新输出的磁盘。完成 了检查点或归档之后,一切又回归正常。数据库之所以暂停用户的活动,这是因为此时已经没地方记录用 户所做的修改了。Oracle 试图重用一个在线重做日志文件,但是由于归档进程尚未完成这个文件的复制
(Archival required),所以 Oracle必须等待(相应地,最终用户也必须等待),直到能安全地重用这个 重做日志文件为止。
如果你看到会话因为一个“日志文件切换”、“日志缓冲区空间”或“日志文件切换检查点或归档未完 成”等待了很长时间,就很可能遇到了这个问题。如果日志文件大小不合适,或者DBWR和ARCH太慢(需 要由DBA或系统管理员调优),在漫长的数据库修改期间,你就会注意到这个问题。我经常看到未定制的“起 始”数据库就存在这个问题。“起始”数据库一般会把重做日志的大小定得太小,不适用较大的工作量(包 括数据字典本身的起始数据库构建)。一旦启动数据库的加载,你会注意到,前1,000行进行得很快,然后 就会呈喷射状进行:1,000 进行得很快,然后暂停,接下来又进行得很快,然后又暂停,如此等等。这些 就是很明确的提示,说明你遭遇了这个问题。
要解决这个问题,有几种做法:
让DBWR更快一些。让你的DBA对DBWR调优,为此可以启用ASYNC I/O、使用DBWR I/O从 属进程,或者使用多个 DBWR进程。看看系统产生的I/O,查看是否有一个磁盘(或一组磁盘) “太热”,相应地需要将数据散布开。这个建议对ARCH也适用。这种做法的好处是,你不用付 出什么代价就能有所收获,性能会提高,而且不必修改任何逻辑/结构/代码。这种方法确实没 有缺点。
增加更多重做日志文件。在某些情况下,这会延迟Checkpoint not complete的出现,而 且过一段时间后,可以把Checkpoint not complete延迟得足够长,使得这个错误可能根本不 会出现(因为你给DBWR留出了足够的活动空间来建立检查点)。这个方法也同样适用于Archival required消息。这种方法的好处是可以消除系统中的“暂停”。其缺点是会消耗更多的磁盘空间 , 但是在此利远远大于弊。
重新创建更大的日志文件。这会扩大填写在线重做日志与重用这个在线重做日志文件之间 的时间间隔。如果重做日志文件的使用呈“喷射状”,这种方法同样适用于Archival required 消息。倘若一段时间内会大量生成日志(如每晚加载、批处理等),其后一段数据却相当平静, 如果有更大的在线重做日志,就能让ARCH在平静的期间有足够的时间“赶上来”。这种方法的 优缺点与前面增加更多文件的方法是一样的。另外,它可能会延迟检查点的发生,由于(至少) 每个日志切换都会发生检查点,而现在日志切换间隔会更大。
让检查点发生得更频繁、更连续。可以使用一个更小的块缓冲区缓存(不太好),或者使用 诸如FAST_START_MTTR_TARGET、LOG_CHECKPOINT_INTERVAL和LOG_CHECKPOINT_TIMEOUT之类的 参数设置。这会强制DBWR更 频繁地刷新输出脏块。这种方法的好处是,失败恢复的时间会减 少。在线重做日志中应用的工作肯定更少。其缺点是,如果经常修改块,可能会更频繁地写至 磁盘。 缓冲区缓存本该更有效的,但由于频繁地写磁盘,会导致缓冲区缓存不能充分发挥作用 , 这可能会影响下一节将讨论的块清除机制。
究竟选择哪一种方法,这取决于你的实际环境。应该在数据库级确定它,要把整个实例都考虑在内。

9.4.5块清除

在这一节中,我们将讨论块清除(block cleanout),即生成所修改数据库块上与“锁定”有关的信 息。这个概念很重要,必须充分理解,在下一节讨论讨厌的ORA-01555:snapshot too old错误时会用到这 个概念。
在第6章中,我们曾经讨论过数据锁以及如何管理它们。我介绍了数据锁实际上是数据的属性,存储 在块首部。这就带来一个副作用,下一次访问这个块时,可能必须“清理”这个块,换句话说,要将这些 事务信息删除。这个动作会生成redo,并导致变脏(原本并不脏,因为数据本身没有修改),这说明一个 简单的SELECT有可能生成redo,而且可能导致完成下一个检查点时将大量的块写至磁盘。不过,在大多 数正常的情况下,这是不会发生的。如果系统中主要是小型或中型事务(OLTP),或者数据仓库会执行直接 路径加载或使用 DBMS_STATS在加载操作后分析表,你会发现块通常已经得到“清理”。如果还记得前面 “COMMIT做什么?”一节中介绍的内容,应该知道,COMMIT时处理的步骤之一是:如果块还在SGA中,就 要再次访问这些块,如果可以访问(没有别人在修改这些块),则对这些块完成清理。这个 活动称为提交 清除(commit cleanout),即清除已修改块上事务信息。最理想的是,COMMIT可以完成块清除,这样后面 的SELECT(读)就不必再清理了。只有块的UPDATE才会真正清除残余的事务信息,由于UPDATE已经在生 成redo,所用注意不到这个清除工作。
可以强制清除不发生来观察它的副作用,并了解提交清除是怎么工作的。在与我们的事务相关的提交 列表中,Oracle会记录已修改的块列表。这些列表都有 20个块,Oracle会根据需要分配多个这样的列表, 直至达到某个临界点。如果我们修改的块加起来超过了块缓冲区缓存大小的10%,Oracle会停止为我们分 配新的列表。例如,如果缓冲区缓存设置为可以缓存3,000 个块,Oracle会为我们维护最多300个块(3,000 的10%)。COMMIT时,Oracle会处理这些包含20个块指针的列表,如果块仍可用,它会执行一个很快的清 理。所以,只要我们修改的块数没有超过缓存中总块数的10%,而且块仍在缓存中并且是可用的,Oracle 就会在COMMIT时清理这些块。否则,它只会将其忽略(也就是说不清理)。
有了上面的理解,可以人为地建立一些条件来查看这种块清除是怎么工作的。我把DB_CACHE_SIZE 设置为一个很低的值4MB,这足以放下512个8KB的块(我的块大小是8KB)。然后创建一个表,其中每行 刚好能在一个块中放下(我不会在每块里放两行)。接下来在这个表中填入了500行,并COMMIT。我要测 量到此为止生成的 redo量。然后运行一个 SELECT,它会访问每个块,最后测量这个SELECT 生成的redo 量。

让许多人奇怪的是,SELECT居然会生成redo。不仅如此,它还会把这些修改块“弄脏”,导致DBWR 再次将块写入磁盘。这是因为块清除的缘故。接下来,我会再一次运行 SELECT,可以看到这回没有生成redo。 这在意料之中,因为此时块都已经“干净”了。
ops$tkyte@ORA10G> create table t
2 ( x char(2000),
3 y char(2000),
4 z char(2000)
5 )
6 /

Table created.
ops$tkyte@ORA10G> set autotrace traceonly statistics ops$tkyte@ORA10G> insert into t
2 select 'x', 'y', 'z'
3 from all_objects
4 where rownum <= 500;
500 rows created. Statistics
----------------------------------------------------------
...
3297580 redo size
...
500 rows processed ops$tkyte@ORA10G> commit;
Commit complete.

以上就是我的表,每个块中一行(我的数据库中块大小为8KB)。现在测量读数据是生成的redo量 :
ops$tkyte@ORA10G> select *
2 from t;
500 rows selected. Statistics
----------------------------------------------------------
...
36484 redo size
...

500 rows processed
可见,这个SELECT在处理期间生成了大约35KB的redo。这表示对T进行全表扫描时修改了35KB的
块首部。DBWR会在将来某个时间把这些已修改的块写回到磁盘上。现在,如果再次运行这个查询:
ops$tkyte@ORA10G> select *
2 from t;
500 rows selected. Statistics
----------------------------------------------------------
...
0 redo size
...

500 rows processed ops$tkyte@ORA10G> set autotrace off
可以看到,这一次没有生成redo,块都是干净的。
如果把缓冲区缓存设置为能保存至少 5,000 个块,再次运行前面的例子。你会发现,无论哪一个 SELECT,生成的redo都很少甚至没有——我们不必在其中任何一个SELECT语句期间清理脏块。这是因为, 我们修改的500个块完全可以在缓冲区缓存的10%中放下,而且我们是独家用户。别人不会动数据,不会 有人导致我们的数据刷新输出到磁盘,也没有人在访问这些块。在实际系统中,有些情况下,至少某些块 不会进行清除,这是正常的。
如果执行一个大的 INSERT(如上所述)、UPDATE 或DELETE,这种块清除行为的影响最大,它会影响 数据库中的许多块(缓存中10%以上的块都会完成块清除)。你会注意到,在此之后,第一个“接触”块的 查询会生成少量的redo,并把块弄脏,如果DBWR已经将块刷新输出或者实例已经关闭,可能就会因为这 个查询而导致重写这些块,并完全清理缓冲区缓存。对此你基本上做不了什么。这是正常的,也在意料之 中。如果Oracle不对块完成这种延迟清除,那么COMMIT的处理就会与事务本身一样长。COMMIT必须重新 访问每一个块,可能还要从磁盘将块再次读入(它们可能已经刷新输出)。
如果你不知道块清除,不明白块清除如果工作,在你看来中可能就是一种好像毫无来由的神秘事务。 例如,假设你更新(UPDATE)了大量数据,然后 COMMIT。现在对这些数据运行一个查询来验证结果。看上 去查询生成了大量写I/O和redo。倘若你不知道存在块清除,这似乎是不可能的;对我来说,第一次看到 这种情况时就是这样认为的,实在是不可思议。然后你请别人一起来观察这个行为,但这是不可再生的, 因为在第二次查询时块又是“干净的”了。这样一来,你就会把它当成是数据库的奥秘之一。
在一个OLTP系统中,可能从来不会看到这种情况发生,因为OLTP系统的特点是事务都很短小,只会 影响为数不多的一些块。根据设计,所有或者大多数事务都短而精。只是修改几个块,而且这些块都会得
一个因素。有些操作会在“干净”的块上创建数据。例如,CREATE TABLE AS SELECT、直接路径加载的数 据以及直接路径插入的数据都会创建“干净”的块。UPDATE、正常的INSERT或DELETE创建的块则可能需 要在第一次读时完成块清除。如果你有如下的处理,就会受到块清除的影响:
将大量新数据批量加载到数据仓库中; 在刚刚加载的所有数据上运行UPDATE(产生需要清理的块); 让人们查询这些数据。
必须知道,如果块需要清理,第一接触这个数据的查询将带来一些额外的处理。如果认识到这一点, 你就应该在UPDATE之 后自己主动地“接触”数据。你刚刚加载或修改了大量的数据;现在至少需要分析 这些数据。可能要自行运行一些报告来验证数据已经加载。这些报告会完成块清 除,这样下一个查询就不 必再做这个工作了。更好的做法是:由于你刚刚批量加载了数据,现在需要以某种方式刷新统计。通过运 行DBMS_STATS实用程序来收集统计,就能很好地清理所有块,这是因为它只是使用SQL来查询信息,会在 查询当中很自然地完成块清除。

9.4.6日志竞争

与cannot allocate new log信息一样,日志竞争(log contention)也是DBA必须修改的问题,一 般要与系统管理员联手。不过,如果DBA检查得不够仔细,开发人员也可以检测到这个问题。
如果你遭遇到日志竞争,可能会看到对“日志文件同步”事件的等待时间相当长,另外 Statspack 报告的“日志文件并行写”事件中写次数(写I/O数)可能很大。如果观察到这种情况,就说明你遇到了 重做日志的竞争;重做日志写得不够快。发生这种情况可能有许多原因。其中一个应用原因(所谓应用原 因是指DBA无法修正这个问题,而必须由开发人员解决)是:提交得太过频繁,例如在重复执行INSERT的 循环中反复提交。在“COMMIT做什么?”一节中我们讲过,如果提交得太频繁,这不仅是不好的编程实践 , 肯定还会引入大量日志文件同步等待。假设你的所有事务都有适当的大小(完全遵从业务规则的要求,而 没有过于频繁地提交),但还是看到了这种日志文件等待,这就有其他原因了。其中最常见的原因如下:
redo放在一个慢速设备上:磁盘表现不佳。该购买速度更快的磁盘了。
redo与其他频繁访问的文件放在同一个设备上。redo设计为要采用顺序写,而且要放在专 用的设备上。如果系统的其他组件(甚至其他Oracle 组件)试图与 LGWR同时读写这个设备, 你就会遭遇某种程度的竞争。在此,只要有可能,你就会希望确保LGWR拥有这些设备的独占访 问权限。
已缓冲方式装载日志设备。你在使用一个“cooked”文件系统(而不是RAW磁盘)。操作系 统在缓冲数据,而数据库也在缓冲数据(重做日志缓冲区)。这种双缓冲会让速度慢下来。如果 可能,应该以一种“直接”方式了装载设备。具体操作依据操作系统和设备的不同而有所变化, 但一般都可以直接装载。
redo采用了一种慢速技术,如RAID-5。RAID-5很合适读,但是用于写时表现则很差。前面 已经了解了COMMIT期间会发生什么,我们必须等待LGWR以确保数据写到磁盘上。倘若使用的 技术会导致这个工作变慢,这就不是一个好主意。
只有有可能,实际上你会希望至少有5个专用设备来记录日志,最好还有第 6个设备来镜像归档日志 。 由于当前往往使用9GB、20GB、36GB、200GB、300GB 和更大的磁盘,要想拥有这么多专用设备变得更加困 难。但是如果能留出4块你能找到的最小、最快的磁盘,再有一个或两个大磁盘,就可以很好地促进LGWR
和ARCH的工作。安排这些磁盘时,可以把它们分为3组(见图9-5): 重做日志组1:磁盘1和磁盘3
重做日志组2:磁盘2和磁盘4
归档:磁盘5,可能还有磁盘6(大磁盘)

图9-5 最优的重做日志配置
将重做日志组1(包括成员A和B)放在磁盘1 和磁盘3 上。把重做日志组2(包括成员C和D)放在 磁盘2 和磁盘4 上。如果还有组3、4等,将分别放在相应的奇数和偶数磁盘组上。这样做的作用是,数据 库当前使用组1 时,LGWR会同时写至磁盘1和3.这一组填满时,LGWR会转向磁盘2和4.等这一组再填满 时,LGWR会回到磁盘1和3.与此同时,ARCH会处理完整的在线重做日志,并讲其写至磁盘5和6(即大磁 盘)。最终的效果是,不论是 ARCH还是LGWR都不会读正在有别人写的磁盘,也不会写正在由别人读的磁盘 , 所以在此没有竞争(见图9-6)。

图9-6 重做日志流
因此,当LGWR写组1时,ARCH在读组2,并写至归档磁盘。当LGWR写组2时,ARCH在读组1,并写 至归档磁盘。采用这种方式,LGWR和ARCH都有各自的专用设备,不会与别人竞争,甚至不会相互竞争。
在线重做日志文件是一组Oracle文件,最适合使用RAW磁盘(原始磁盘)。如果说哪种类型的文件可 以考虑使用原始分区(RAW),首先其冲地便是日志文件。关于使用原始分区和cooked文件系统的优缺点, 这方面的讨论很复杂。由于这不是一本有关DBA/SA认为的书,所以我不打算过分深入。但是要指出,如果 你要使用RAW设备,在线重做日志文件就是最佳候选。在线重做日志文件不用备份,所以将在线重做日志 文件放在RAW分区上而不是cooked文件系统上,这不会影响你的任何备份脚本。ARCH总能把RAW日志转 变为cooked文件系统文件(不能使用一个RAW设备来建立归档),在这种情况下,就大大减少了RAW设备 的“神秘感”。

9.4.7临时表和 redo/undo

一般认为临时表(temporary table)还是Oracle中一个相当新的特性,只是在Oracle8i 8.1.5版 本中才引入。因此,有关临时表还存在一些困惑,特别是在日志方面。我们将在第10章介绍如何以及为什 么使用临时表。这一节只是要回答这样一个问题:“关于生成修改日志,临时表是怎样做的?”
临时表不会为它们的块生成redo。因此,对临时表的操作不是“可恢复的”。修改临时表中的一个块 时,不会将这个修改记录到重做日志文件中。不过,临时表确实会生成 undo,而且这个undo会计入日志。 因此,临时表也会生成一些redo。初看上去好像没有道理:为什么需要生成undo?这是因为你能回滚到事 务中的一个SAVEPOINT。可以擦除对临时表的后50个INSERT,而只留下前50个。临时表可以有约束,正 常表有的一切临时表都可以有。可能有一条INSERT语句要向临时表中插入500行,但插入到第500行时失 败了,这就要求回滚这条语句。由于临时表一般表现得就像“正常”表一样,所以临时表必须生成undo。 由于undo数据必须建立日志,因此临时表会为所生成的undo生成一些重做日志。
这样似乎很不好,不过没有你想像中那么糟糕。在临时表上运行的 SQL语句主要是INSERT和SELECT。 幸运的是,INSERT只生成极少的undo(需要把块恢复为插入前的“没有”状态,而存储“没有”不需要多
少空间),另外 SELECT根本不生成undo。因此,如果只使用临时表执行INSERT和SELECT,这一节对你来 说意义不大。仅当要对临时表执行UPDATE或DELETE时,才需要关心这一节的内容。
我建立了一个小测试来演示使用临时表时生成的redo量,同时这也暗示了临时表生成的undo量,因 为对于临时表,只会为undo生成日志。为了说明这一点,我采用了配置相同的“永久”表和“临时”表 , 然后对各个表执行相同的操作,测量每次生成的redo量。这里使用的表如下:
ops$tkyte@ORA10G> create table perm
2 ( x char(2000) ,
3 y char(2000) ,
4 z char(2000) )
5 /
Table created.
ops$tkyte@ORA10G> create global temporary table temp
2 ( x char(2000) ,
3 y char(2000) ,
4 z char(2000) )
5 on commit preserve rows
6 /
Table created.

我建立了一个小的存储过程,它能执行任意的SQL,并报告 SQL生成的redo量。我会使用这个例程

分别在临时表和永久表上执行INSERT、UPDATE和DELETE:
ops$tkyte@ORA10G> create or replace procedure do_sql( p_sql in varchar2 )
2 as
3 l_start_redo number;
4 l_redo number;
5 begin

6 select v$mystat.value
7 into l_start_redo
8 from v$mystat, v$statname
9 where v$mystat.statistic# = v$statname.statistic#
10 and v$statname.name = 'redo size';
11
12 execute immediate p_sql;
13 commit;
14
15 select v$mystat.value-l_start_redo
16 into l_redo
17 from v$mystat, v$statname
18 where v$mystat.statistic# = v$statname.statistic#
19 and v$statname.name = 'redo size';
20
21 dbms_output.put_line
22 ( to_char(l_redo,'9,999,999') ||' bytes of redo generated for
"' ||
23 substr( replace( p_sql, chr(10), ' '), 1, 25 ) || '"...' );
24 end;
25 /
Procedure created.

接下来,对PERM表和TEMP表运行同样的INSERT、UPDATE和DELETE:
ops$tkyte@ORA10G> set serveroutput on format wrapped
ops$tkyte@ORA10G> begin

2 do_sql( 'insert into perm
3 select 1,1,1
4 from all_objects
5 where rownum <= 500' );
6
7 do_sql( 'insert into temp
8 select 1,1,1
9 from all_objects
10 where rownum <= 500' );
11 dbms_output.new_line;
12
13 do_sql( 'update perm set x = 2' );
14 do_sql( 'update temp set x = 2' );
15 dbms_output.new_line;
16
17 do_sql( 'delete from perm' );
18 do_sql( 'delete from temp' );
19 end;
20 /
3,297,752 bytes of redo generated for "insert into perm "...
66,488 bytes of redo generated for "insert into temp "...
2,182,200 bytes of redo generated for "update perm set x = 2"...
1,100,252 bytes of redo generated for "update temp set x = 2"...

3,218,804 bytes of redo generated for "delete from perm"...
3,212,084 bytes of redo generated for "delete from temp"...
PL/SQL procedure successfully completed.
可以看到:
对“实际”表(永久表)的INSERT生成了大量redo。而对临时表几乎没有生成任何redo。 这是有道理的,对临时表的INSERT只会生成很少的undo数据,而且对于临时表只会为undo数 据建立日志。
实际表的UPDATE生成的redo大约是临时表更新所生成redo的两倍。同样,这也是合理的 。 必须保存UPDATE的大约一半(即“前映像”)。对于临时表来说,不必保存“后映像”(redo)。
DELETE需要几乎相同的redo空间。这是有道理的,因为对 DELETE的undo很大,而对已修 改块的redo很小。因此,对临时表的DELETE与对永久表的DELETE几乎相同。
注意 你看到INSERT语句在临时表上生成的redo比在永久表上生成的redo还多,这实际上是数据库 产品本身的问题,这个问题至少在Oracle9.2.0.6和10.1.0.4补丁版中(编写这本书时发布的当 前版本)中得到了修正。
因此,关于临时表上的DML活动,可以得出以下一般结论: INSERT会生成很少甚至不生成undo/redo活动。
DELETE在临时表上生成的redo与正常表上生成的redo同样多。 临时表的UPDATE会生成正常表UPDATE一半的redo。
对于最后一个结论,需要指出有一些例外情况。例如,如果我用2,000 字节的数据 UPDATE(更新) 完全为NULL的一列,生成的undo数据就非常少。这个UPDATE表现得就像是INSERT。另一方面,如果我 把有2,000 字节数据的一列UPDATE为全NULL,对redo生成来说,这就表现得像是DELETE。平均来讲,可 以这样认为:临时表UPDATE与实际表UPDATE生成的undo/redo相比,前者是后者的50%。
一般来讲,关于创建的redo量有一个常识。如果你完成的操作导致创建undo数据,则可以确定逆向 完成这个操作(撤销操作)的难易程度。如果INSERT2,000字节,逆向操作就很容易,只需回退到无字节 即可。如果删除了(DELETE)2,000字节,逆向操作就是要插入2,000字节。在这种情况下,redo量就很 大。
有了以上了解,你可能会避免删除临时表。可以使用TRUNCATE(当然要记住,TRUNCATE是DDL,而 DDL会提交事务,而且在Oracle9i及以前版本中,TRUNCATE还会使你的游标失效),或者只是让临时表在 COMMIT之后或会话终止时自动置空。执行方法不会生成undo,相应地也不会生成redo。你可能会尽量避 免更新临时表,除非由于某种原因必须这样做。你会把临时表主要用于插入(INSERT)和选择(SELECT)。 采用这种方式,就能更优地使用临时表不生成redo的特有能力。