ORA-01555错误是一种在Oracle数据库中很常见的错误。尤其在Oracle 8i及之前的版本最多。从9i开始的undo自动管理,至现在的10g、11g中的undo auto tuning,使得ORA-01555的错误越来越少。但是这个错误,仍然不可避免。而出现ORA-01555错误,通常有2种情况:

  • SQL语句执行时间太长,或者UNDO表空间过小,或者事务量过大,或者过于频繁的提交,导致执行SQL过程中进行一致性读时,SQL执行后修改的前镜像(即UNDO数据)在UNDO表空间中已经被覆盖,不能构造一致性读块。
  • SQL语句执行过程中,访问到的块,在进行延迟块清除时,不能确定该块的事务提交时间与SQL执行开始时间的先后次序。

第1种情况,是最常见的。解决的办法无非就是增加UNDO表空间大小,优化出错的SQL,或者避免频繁地提交。而第2种情况则是比第1种情况少很多。下面简单描述一下第2种情况发生的情景:

  • 有事务大量修改了A表的数据,或者A表的数据虽然被事务少量修改,但是一部分修改过的块已经刷出内存并写到了磁盘上。随即事务提交,提交时刻为SCN1。而提交时有数据块上的事务没有被清除。
  • 在SCN2时刻,开始执行SELECT查询A表,对A表进行全表扫描,而且A表很大。也可能是其他情况,比如是小表,但是是一个游标方式的处理过程,而处理过程中又非常耗时。注意,这里SCN2与SCN1之间可能相隔了很远,从时间上来说,甚至可能有数十天。不管怎么样,这在SCN1至SCN2时间之间,系统中存在大量的事务,使得UNDO表空间的块以及UNDO段头的事务表全部被重用过。
  • SELECT语句在读A表的一个块时,发现表上有活动事务,这是由于之前的事务没有清除所致。ORACLE根据数据块中ITL的XID检查事务表,这时会有2种情况:
    • XID对应的事务表中的记录仍然存在并发现事务已经提交,可以得到事务准确的提交SCN(commit scn),称为SCN3,等于SCN1。很显然,由于查询的时刻SCN2晚于事务提交的时刻SCN1,那么不需要构造一致性读块。
    • XID对应的事务表中的记录已经被重用,这个时候仍然表明表明事务已经被提交。那么这个时候,Oracle没办法准确地知道事务的提交时间,只能记录为这样一个事实,事务提交的SCN小于其UNDO段的事务表中最近一次重用的事务记录的SCN(即这个事务表最老的事务SCN)。这里称这个SCN为SCN4。
  • SCN4可能远小于SCN2,那是因为事务很早之前就已经提交。也可能SCN4大于SCN2,这是因为SELECT语句执行时间很长,同时又有大量的事务已经将事务表重用。对于后者,很显然,Oracle会认为该事务的提交时间可能在SELECT开始执行之后。这里为什么说可能,是因为ORACLE只能判断出事务是在SCN4之前提交的,并不是就刚好在SCN4提交。而此时,利用UNDO BLOCK进行一致性读数据的构造也很可能失败,因为UNDO BLOCK很可能已经被覆盖,特别是SCN1远小于SCN2的情况下。在这种情况下,ORA-01555错误就会出现。

对于上面最后一段,在SCN4大于SCN2的情况下,之后的描述,我提到了几个“可能”,是因为我对此也不能完全确定,Oracle是否还会有其他的方法来判断 事务的提交时间早于SCN2。而我自己的模拟测试始终没有模拟出ORA-01555。我的测试过程是这样子的:

  • 修改表T1,注意T1表已经足够大,比如几十万行数据以上。
  • flush buffer_cache,使未提交的事务修改的块全部刷出内存。
  • 提交事务。
  • 使用大量的事务(注意这些事务不含表T1),将UNDO表空间填满并确保所有事务表已经被全部重用过。
  • 写一段代码,以游标方式打开表T1,在游标的循环中使用dbms_lock.sleep故意增加时间。
  • 同时多个JOB会话产生大量与表T1无关的事务,将UNDO表空间填满并确保所有事务表已经被全部重用过。

在我的期望中,上面的测试,对于游标处理部分,应该会报ORA-01555错误。但实际测试并没有出现,对于这类情形,看起来Oracle还有其他的机制来发现块上的事务提交时间早于查询开始时间。

虽然测试没有达到预期的结果,但是对于事务提交后块没有清除引起的ORA-01555错误,需要几个充分的条件:表足够大,表上的事务提交后有没有事务清除的块,对大表进行长时间的查询比如全表扫描,查询开始后有大量的事务填充和UNDO表空间和重用了事务表。

而下面则是延迟块清除时引起的ORA-01555错误的一则案例。

首先进行简单的环境介绍,运行在HP-UX环境下的Oracle 10.2.0.3,主机只有4颗比较老的PA-RISC CPU。这个系统的特点是大数据量的批量处理,基本上都是大数据量的插入。每个月的数据有单独的表,表一般都是在几十G以上,大的表超过100G。CPU利用率长期保持在100%。

由于空间限制,需要定期将一些N个月之前的表导出备份到磁带上,然后将表删除。这些表,在导出时是不可能会有DML操作的。由于性能原因,导出时间过长(几个小时以上),在导出时经常会遇到ORA-01555错误而失败。这里不讨论怎么样提高性能使导出时间更短,这里只提出一种方法来解决ORA-01555错误。

从之前对ORA-01555错误的成因分析可以知道,这个ORA-01555错误,正是由于表上存在未清除的事务,同时导出时间过长,UNDO段头的事务表被全部重用,ORACLE在查询到有未清除事务的块时不能确定事务提交时间是否早于导出(查询)开始时间,这时候就报ORA-01555错误。

要解决这个错误,除了提高性能,那么从另一个角度来思考这个问题,可以想办法先清除掉表上的事务(即延迟块清除)。那么我们可以通过一个简单的SELECT语句来解决:

SELECT /*+ FULL(A) */ COUNT(*) FROM BIG_TABLE A;

SELECT COUNT(*),速度显然大大高于SELECT *,所需的时间也更短,出现ORA-01555错误的可能性就非常低了。

注意这里需要加上FULL HINT,以避免查询进行索引快速全扫描,而不是对表进行全表扫描。另外,需要注意的是,这里不能为了提高性能而使用PARALLEL(并行),测试表明,在表上进行并行查询,以DIRECT READ方式读取表并不会清除掉表上的事务。

如果表过大,SELECT COUNT(*)的时间过长,那么我们可以用下面的代码将表分成多个段,进行分段查询。


  select dbms_rowid.rowid_create(1, oid1, fid1, bid1, 0) rowid1,
         dbms_rowid.rowid_create(1, oid2, fid2, bid2, 9999) rowid2
    from (select a.*, rownum rn
            from (select chunk_no,
                         min(oid1) oid1,
                         max(oid2) oid2,
                         min(fid1) fid1,
                         max(fid2) fid2,
                         min(bid1) bid1,
                         max(bid2) bid2
                    from (select chunk_no,
                                 FIRST_VALUE(data_object_id) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) oid1,
                                 LAST_VALUE(data_object_id) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) oid2,
                                 FIRST_VALUE(relative_fno) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) fid1,
                                 LAST_VALUE(relative_fno) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) fid2,
                                 FIRST_VALUE(block_id) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) bid1,
                                 LAST_VALUE(block_id + blocks - 1) OVER(PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) bid2
                            from (select data_object_id,
                                         relative_fno,
                                         block_id,
                                         blocks,
                                         ceil(sum2 / chunk_size) chunk_no
                                    from (select /*+ rule */ b.data_object_id,
                                                 a.relative_fno,
                                                 a.block_id,
                                                 a.blocks,
                                                 sum(a.blocks) over(order by b.data_object_id, a.relative_fno, a.block_id) sum2,
                                                 ceil(sum(a.blocks)
                                                      over() / &trunks) chunk_size
                                            from dba_extents a, dba_objects b
                                           where a.owner = b.owner
                                             and a.segment_name = b.object_name
                                             and nvl(a.partition_name, '-1') =
                                                 nvl(b.subobject_name, '-1')
                                             and b.data_object_id is not null
                                             and a.owner = upper('&owner')
                                             and a.segment_name = upper('&table_name'))))
                   group by chunk_no
                   order by chunk_no) a);

在上面的代码中trunks变量表示表分为的段数。
代入trunks,owner,table_name三条SQL,执行上面的代码,出来的结果类似如下:

ROWID1             ROWID2
------------------ ------------------
AAAER9AAIAAABGJAAA AAAER9AAIAAABIICcP
AAAER9AAIAAABIJAAA AAAER9AAIAAABMICcP
AAAER9AAIAAABMJAAA AAAER9AAIAAABQICcP
AAAER9AAIAAABQJAAA AAAER9AAIAAABWICcP
AAAER9AAIAAABWJAAA AAAER9AAIAAABaICcP
AAAER9AAIAAABaJAAA AAAER9AAIAAABcICcP
AAAER9AAIAAABcJAAA AAAER9AAIAAABgICcP
AAAER9AAIAAABgJAAA AAAER9AAIAAABkICcP
AAAER9AAIAAABkJAAA AAAER9AAIAAABoICcP
AAAER9AAIAAABoJAAA AAAER9AAIAAABsICcP

然后对每一个ROWID段执行类似下面的SQL:

SELECT /*+ NO_INDEX(A) */ COUNT(*) FROM BIG_TABLE A WHERE ROWID>='AAAER9AAIAAABGJAAA' AND ROWID< ='AAAER9AAIAAABIICcP';

对表进行分段处理,除了此处的用法,完全可以用于手工多进程处理大批量数据。更完整的功能已经在11g中实现,此处不做过多介绍。

对于本文提到的导出数据遇到ORA-01555错误的表,按上述方法处理后,问题得到解决,表顺利导出。

Trackback

18 comments untill now

  1. 很经典!

  2. 解释的很详细,我们也碰到过这种问题。
    有个疑问:如果在导出过程中该表又有大事务存在,用并行去导出,可以将sga中的跟该表有关的buffer尽快刷出去,这样导出就不需要构建cr块了,应该也能降低导出时间吧。
    上面那个分trunck的sql,将count(*)改成具体的字段就可以并行导出了,应该也能降低时间。
    “Oracle是否还会有其他的方法来判断 事务的提交时间早于SCN2”,这个我同事推测即使scn4>scn2,但只要对应的undo block还在,就能构建cr块,就不会报ORA-01555了。也不确认

    老熊 Reply:

    @mq44944, 就算是用并行,被事务修改后同样需要构建CR块,不管是conventional read 还是direct path read,一致性读的原则不能改变。
    “但只要对应的undo block还在,就能构建cr块”,这个的确是有可能的。

    “将count(*)改成具体的字段就可以并行导出了”,不太明白这个说法的意思。

  3. 谢谢回复,我明白了,我们是通过并行解决的,看来降低时间只是并行的原因,cr块构建是不可避免的。
    我说替换count(*),可能就是指你说的手工多进程根据rowid范围分批导出,还似乎oracle 并行操作简单点,请忽略。

    ps:这个blog服务器的时间时区是不是不对?

    老熊 Reply:

    @mq44944, 其实分段导出也是可以的,之前有几次类似的错误就是这么做的。在这篇文章中只是列出了另外一种思路。

  4. 老大,你的odu 3.09下载不了啊!能不能给我发一份windows版的?谢谢

    老熊 Reply:

    @wzq, 请与我邮件或QQ联系。

  5. Kevin.Zhang @ 2011-03-25 12:50

    data block中延迟提交的那个事务T1
    占据了T1的事务表的事务是T2
    你最后测试想要模拟ORA-1555的那个是T3

    虽然T1的提交时间已经不可查,但是有一点是确定的,就是如果T1的提交时间必然小于T2的开始的SCN。
    所以你T3的开始时间如果大于T2的开始时间,则不会报ORA-1555.

    老熊可以试着从T2的SCN上入手。将其改大点。

    老熊 Reply:

    @Kevin.Zhang, 生产库是不可能允许修改什么SCN的。
    这里的问题恰恰就是因为查询执行时间太长,导致事务表中最早提交的事务的SCN都大于查询开始时间。

  6. “事务提交的SCN小于其UNDO段的事务表中最近一次重用的事务记录的SCN(即这个事务表最老的事务SCN)”
    是写错了
    应当是事务提交的SCN小于其UNDO段的事务表中最远一次重用的事务记录的SCN,只有最远一次才是事务表最老的事务SCN吧?

    老熊 Reply:

    @jay, 当然,“最远”一次重用的事务的SCN比最近一次重用的事务的SCN小。但是“最远”到底是多远呢?实际上我这里的意思是,“最近”一次“被”重用的事务的SCN,在目前的事务表中是最老的。

  7. @老熊

    昨天在google reader看到这篇文章又输出了,重新看了一遍, 好像过段时间有新的回复就会输出, 呵呵. 我做了一个实验模拟了因为延迟块清除而出现的ORA-01555, 整理放在blog上.
    http://sid.gd/ora-01555_deplaye_block_cleanout/

    老熊 Reply:

    @Sidney, 最近是有点奇怪,不知道为什么会把老文章又输出到了Google Reader,看了你的文章,非常棒。

  8. 兄弟,我想清楚了。Oracle做延迟块清除的时候不一定需要一致读,关键看执行select操作的那个时间点的SCN和undo段头中记录的control scn的比对结果,如果select scn > control scn,这里就执行常规的延迟块清除就好了,也不需要做任何的一致读和回滚undo段头中transaction table的操作;只有当select scn < control scn的时候,这时候Oracle就不知道到底要不要做一致读了,所以Oracle这里必须要去回滚整个undo段头的transaction table——只有这样才能判断出是否需要做一致读,而如果在回滚transaction table的过程中发现回滚不了了(这当然因为相关的undo record被覆盖了),这时候Oracle就会报错ORA-01555;所以延迟块清除的时候回滚transaction table不是为了得到准确的commit SCN(实际上,延迟块清除的机理一直就是根本就不需要得到准确的commit SCN),而是为了判断到底需不需要做一致读。

    老熊 Reply:

    @dbsnake, 延迟块清除的确只是为了表示事务已经提交了,但是它还是要尽量保证能够得到足够精确的提交SCN,来为一致性读服务。

  9. 熊哥:
    请教一下,你上例举的分段进行查询表的目的是为了让表中所有的块都进行延迟块清除?且分段查询是为了降低发生ora-01555的概率(过长查询还可能造成查询scn小于undo header 最小scn)?
    是否要让分段执行的所有sql都完全执行才能保证不发生ora-01555?
    如果比较背的话,在一个分段中有n个块需要延迟块清除,且每个延迟块清除都引发了ora-01555,岂不是要 重复查询n回?

    多谢!

    老熊 Reply:

    本文举的例子,主要是由于exp导出性能较差(由于机器性能的原因),导出时间特别长。而我们构造出来的分段查询,主要是希望它能在短时间内完成,这样在这段时间内就不会有太多的事务去覆盖UNDO段头的事务表以及填充完所有的UNDO块。如果查询的时候够短,不会出现你说的那种比较背的情况(除非UNDO段过小同时当时事务量特别多)。

  10. […] 在《ORA-1555错误解决一例》一文中,当时尝试模拟UNDO段头事务表被覆盖的情况下出现ORA-01555错误,没有成功。实际上没有成功的原因是事务数虽然多,但是事务过小,使UNDO块没有被覆盖完,这样通过回滚事务表仍然能够得到事务表以前的数据。本文进一步讨论一些有关延迟块清除和一致性读方面的内容(但不会涉及到延迟块清除和一致性读的具体概念和过程,只是一些有趣的东西)。 […]

Add your comment now