1. 概述:备库查询弹出ORA-01555,问题可能比你想象的复杂
做DBA的人,对ORA-01555应该都不陌生——快照过旧,典型的UNDO不够用或者查询时间太长导致的一致性读失败。但大部分人对这个报错的认知都停留在主库上,觉得备库只承担只读查询,又不产生事务,怎么会冒出来01555?
我在实际运维中接过不少这样的工单,现象非常一致:应用通过ADG备库跑某个批处理报表,查询跑了二三十分钟,突然报出ORA-01555: snapshot too old,应用直接失败。主库上同样的SQL好好的,备库却偶发报错,而且重启备库、清理会话都没用,过几天又冒出来。
这个问题的根子,在于你下意识地把备库当成了“一个性能稍弱的主库”来理解,但ADG备库在UNDO管理上和主库有本质差异。它不只是性能弱,而是UNDO段的行为逻辑都不一样。要真正解决备库上的01555,不能只是加大UNDO表空间、调大undo_retention这么简单,尤其是在Oracle 19c之后的ADG环境下,有更优雅也更彻底的方案。
这篇文章我就从问题产生的底层机制聊起,把备库只读查询下UNDO的工作原理、常见误区和一条完整的优化路径都梳理清楚,最后附上可以直接拿去用的监控和排查脚本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 备库出现ORA-01555的根本原因:为什么备库明明“不写”却会快照过旧
2.1 快照过旧的本质是UNDO被覆盖,而不是UNDO空间用完
要理解备库为什么会产生ORA-01555,先得捋清楚这个报错的机制到底是什么。
Oracle的多版本读一致性(MVCC)模型下,一个查询开始的那一刻,数据库会记录一个查询SCN。查询每读取一个数据块,都需要保证看到的是这个SCN时刻的数据版本。如果数据块已经被其他事务修改,数据库就去UNDO段里找修改前的镜像,一层一层往回翻滚,构造出查询SCN时刻的版本。这个过程就是一致性读(Consistent Read,CR)。
问题来了:UNDO段是循环使用的。一个事务提交之后,它产生的UNDO信息并不会立刻被清理,而是要等没有任何活跃查询还需要它的时候才能被覆盖。如果某个查询运行时间太长,或者某个小UNDO表空间撑不住高并发的事务量,后续的事务就会把前面已经提交事务的UNDO覆盖掉。这时候那个长时间运行的查询再想去读历史版本,发现UNDO里已经找不到了,数据库就只能抛出ORA-01555。
注意这里有个关键点:ORA-01555和UNDO表空间满不满没有必然关系。UNDO表空间即使使用率只有30%,只要某个回滚段里的数据被覆盖了,一样会报01555。所以网上很多“扩UNDO表空间”的建议,其实只解决了空间不够导致的覆盖问题,没有解决根本的“覆盖行为”。
2.2 ADG备库的UNDO机制:非本地事务产生的UNDO,备库管不了
主库上的UNDO是主库自己的事务生成的,自己管理、自己清理,逻辑上是闭环的。但ADG备库完全不一样。
ADG备库通过日志应用(Managed Recovery)重放主库传过来的Redo日志,从而在备库上同步数据。Redo日志里包含了数据块的变更,也包括UNDO段的变更。也就是说,备库上的UNDO段内容和主库是完全一致的——主库产生多少UNDO,备库就会应用多少UNDO。备库上的UNDO,实际上是在重放主库的事务时被动生成的,备库对UNDO段没有自主管理权。
这就导致了一个在备库上非常棘手的局面:备库上的UNDO段能否被覆盖,取决于主库上对应UNDO段的覆盖情况。主库如果因为某些原因提前覆盖了UNDO,备库同步应用后也会把这部分UNDO覆盖掉。而备库上的只读查询会话,在这期间可能还在使用那些已经被覆盖的UNDO来做一致性读。
打个不严谨但好理解的比方:主库是图书馆的管理员,负责决定哪些旧报刊留档、哪些可以清理丢掉;备库是另一个阅览室,管理员的所有清理动作都会同步执行过来。如果阅览室里有个读者正捧着一本已经被管理员判定“可以丢”的旧报纸在仔细研读,那对不起,报纸被丢掉的那一刻,读者手里的内容就残缺了。
更复杂的情况是Data Guard的日志应用存在延迟。备库在应用日志时不会考虑备库本地有多少长查询在跑,它只会按照主库日志里的时间线一板一眼地重放。所以备库上运行得越久的只读查询,就越容易撞上一个尴尬时刻——它需要读取的UNDO信息刚好被备库应用过来的Redo覆盖掉。
2.3 “最小补充日志”与延迟清除:一个经常被忽略的辅助因素
除了UNDO覆盖这个核心原因,还有一个参数在ADG场景下会加重01555的概率——DELAY和UNDO_RETENTION在备库上的表现差异。
主库上设置UNDO_RETENTION,代表的是“已提交的UNDO信息至少保留N秒不覆盖”。这个参数在备库上是生效的,但效果完全不同。主库的UNDO_RETENTION可以尽量阻止UNDO被覆盖,但备库的UNDO清除行为依赖日志应用过程重新执行一遍UNDO段的覆盖逻辑,备库本地对UNDO_RETENTION的调整能力非常有限。
我从实践中总结的经验是:如果你在备库上发现有00155报错,先不要急着去调备库的UNDO参数。在19c之前,备库上很多UNDO参数改了根本不生效,因为备库的UNDO段是从主库Redo里重建出来的,不是本地初始化的。这也导致一个很反直觉的结论:备库的UNDO表空间即使你扩得再大,可能也只能缓解一时,不能根治01555。
2.4 一个真实故障的复盘:主库没事备库报错的全过程
有一次我处理过一个案例,可以很好地说明这个问题的隐蔽性。
某个客户的核心系统是Oracle 19.16的ADG双机架构,备库承担着大量报表和查询业务。某天下午,备库上连续有多个报表任务报ORA-01555。从错误日志看,报错的SQL是一条很复杂的多表关联查询,单次执行时间大概在25分钟到40分钟之间。主库上同样的查询没有任何问题。
初步排查时,DBA按照传统经验把备库的UNDO_RETENTION调到了3600秒(此前是900秒),同时给备库的UNDO表空间增加了20GB的数据文件。结果第二天,同样的报错又出现了。
后来我们拉出主库的UNDO使用曲线,发现一个规律:主库上每天下午有一个批量任务,会在短时间内更新一张大表,产生大量UNDO。主库的UNDO_RETENTION虽然设置了900秒,但因为这批事务的量太大,UNDO表空间自动扩展有延迟,导致已经提交的事务在900秒内就被覆盖了。主库自身没有长查询,所以根本不会感知到这个问题。但备库上那些正在跑的报表查询,恰恰需要回看这批被覆盖的UNDO数据,于是01555就在备库上集中爆发了。
这个案例很典型地说明了:备库的ORA-01555本质上往往是主库UNDO压力传导过来的结果,单修备库是治标不治本。
3. 定位备库ORA-01555的排查路径:确认问题边界与UNDO状态
3.1 第一步:确认报错源在备库本地还是主库日志
ADG环境下出现ORA-01555,首先要确认错误到底是在备库本地产生的,还是在主库产生后通过日志同步过来的。
这个判断很简单,查询备库的Alert日志,如果报错信息前面带有“Errors in file”且路径指向备库自身的trace目录,同时报错时间点和应用的报错时间能对上,那基本可以确认是备库本地查询触发的01555。还有一种情况是主库自身在做某些操作时产生01555,这个错误会记录在主库的Alert日志中,但不会导致备库报错——除非主库因为这个错误发生了其他连锁反应。
我建议判断标准就一句话:备库Alert日志里能直接看到ORA-01555,且伴随有“kdifind: no undo record for snapshot”这类信息,那铁定是备库本地的一致性读失败了。
3.2 第二步:确认报错SQL的持续时间和UNDO使用情况
确认是备库本地问题后,下一步要拉出报错SQL的执行时长和涉及表的UNDO使用情况。这里有一个SQL可以直接查:
sql复制-- 查询最近一段时间内UNDO段使用情况
SELECT TO_CHAR(BEGIN_TIME, 'HH24:MI:SS') AS begin_time,
TO_CHAR(END_TIME, 'HH24:MI:SS') AS end_time,
undoblks,
txncount,
maxquerylen,
maxconcurrency
FROM v$undostat
WHERE begin_time > SYSDATE - 1
ORDER BY begin_time DESC;
重点看几个列:MAXQUERYLEN表示在对应时间段内运行时间最长的查询的时长(单位秒),如果这个值超过了UNDO_RETENTION设置,就说明系统内确实存在超长查询。UNDOBLKS反映的是UNDO产生的速率,如果某个时间段UNDO产生量特别大,那这个时间段前后的长查询最容易出问题。
注意,ADG备库上v$undostat的数据其实是从主库同步过来的UNDO活动统计。所以这个视图在备库上看到的,不完全是备库本地的UNDO使用情况。备库本地也有少量UNDO活动,比如备库上通过临时UNDO产生的排序、局部临时表操作。但要记住,主库传过来的UNDO占用才是大头。
3.3 第三步:判断UNDO覆盖是否来自主库批量事务
如果备库上确实有长查询在跑,且报错时间段和主库的UNDO高峰重叠,那基本可以锁定是“主库UNDO提前覆盖”导致的。
我习惯用下面这个SQL去查主库最近一天UNDO的产生量和覆盖情况:
sql复制-- 在备库上同样可以执行,但反映的是主库同步过来的UNDO活动
SELECT TO_CHAR(begin_time, 'MMDD HH24:MI') AS begin_time,
undoblks,
txncount,
maxquerylen,
ROUND(undoblks / 600, 2) AS undo_per_sec
FROM v$undostat
WHERE begin_time > SYSDATE - 1
ORDER BY undoblks DESC;
如果看到某几个时间段UNDOBLKS异常大,再把这些时间点去和主库的定时任务执行记录核对。通常就会发现,01555报错的时间点,往往紧跟在这些UNDO高峰之后半小时到一小时之内。
3.4 第四步:检查UNDO表空间配置与自动扩展能力
还有一种容易被忽视的情况:UNDO表空间设置了固定的数据文件大小,没有开启自动扩展。
主库的UNDO表空间如果用完了,会触发数据文件的自动扩展,但如果 AUTOEXTEND 关闭了,Oracle会在UNDO空间耗尽时直接复用最旧的UNDO区——即使此时还有活跃事务引用着这些UNDO。这就等于数据库自己主动把还在使用的UNDO给覆盖了,快照过旧就不可避免。
所以排查时也要检查一下UNDO表空间的AUTOEXTEND配置:
sql复制SELECT tablespace_name,
file_name,
autoextensible,
bytes / 1024 / 1024 AS size_mb,
maxbytes / 1024 / 1024 AS max_mb
FROM dba_data_files
WHERE tablespace_name LIKE 'UNDO%';
如果AUTOEXTENSIBLE是NO,优先把它打开,同时设置一个合理的MAXSIZE。这是最基础的防御手段。但请记住,这只能防止UNDO空间不够用导致的覆盖,防不住主库事务量过大导致的提前覆盖。
4. 常规修复无效的关键认知:备库UNDO为什么不听你指挥
4.1 备库UNDO参数调整存在严重限制
在深入优化方案前,必须先把这个认知建立起来:传统意义上对UNDO的调优手段,在ADG备库上大部分是无效的,有些甚至根本改不了。
主库上的UNDO调优三板斧是:扩大UNDO表空间、调高UNDO_RETENTION、设置UNDO表空间的RETENTION GUARANTEE。但在ADG备库上,这三招的效力都大打折扣。
第一个限制是参数层面。UNDO_RETENTION是静态参数还是动态参数取决于版本和配置,但在ADG环境的备库实例上,一些UNDO相关的参数修改会被直接拒绝,或者需要重启才能生效。原因是备库的UNDO段在Mount阶段由MRP进程初始化,它的属性跟着主库走。备库上你改了UNDO_RETENTION,等MRP重新应用日志时,可能又被主库传来的UNDO段属性覆盖回去。
第二个限制更隐蔽:即使你能在备库上把UNDO_RETENTION调得很大,备库的UNDO段空间计算方式是全局的,它不能区分“这份UNDO主库还在用”和“这份UNDO主库已经不需要了”。备库只能被动接受主库传过来的UNDO块信息,主库说覆盖就覆盖。
所以在ADG备库上调UNDO_RETENTION,有效果,但效果远不如主库那么直接。这也是很多DBA折腾了很久发现“明明调了参数还是报错”的原因。
4.2 RETENTION GUARANTEE在ADG环境下是个伪命题
UNDO表空间的RETENTION GUARANTEE属性,在大多数资料里都被描述为“保证未提交的UNDO不会被覆盖”。但在ADG备库上,这个属性基本是个摆设。
为什么?因为GUARANTEE的核心机制是通过主动阻塞新事务来保护旧UNDO不被覆盖。备库上如果启用了GUARANTEE,理论上当MRP应用Redo日志需要覆盖UNDO时,数据库会检测到冲突。但备库的MRP进程本质上是单线程的恢复前滚过程,它如果被阻塞了,就会导致日志应用中断,备库数据延迟无限增大,最终主备同步中断。
所以你会发现一个很有意思的现象:许多ADG最佳实践文档里都建议备库上关闭RETENTION GUARANTEE,目的就是避免MRP进程因为UNDO冲突被阻塞,进而导致备库不可用。如果开启了GUARANTEE,备库在极端情况下会报ORA-30036“unable to extend segment by ... in undo tablespace”,日志应用直接挂起。
无论如何,你都不能让备库为了保查询而牺牲日志同步,因为一个延迟的备库比一个偶尔报01555的备库更危险。
4.3 那要不要加大备库UNDO表空间?
这个操作本身没有错,它可以给UNDO覆盖提供更大的缓冲区间。但你需要对它的实际效果有一个清醒的预期。
备库的UNDO表空间扩得再大,它也只是让“可循环覆盖的空间”变大,不会改变“何时覆盖”的时间点。只有备库本地的UNDO段内容会在备库上保留得更久一点。但备库应用主库日志时会通过覆盖操作持续推进,UNDO段里旧的数据会被新应用的数据替代。表空间大了,可能只是让覆盖循环的周期变长,并不能保证你在跑的查询与覆盖操作“错开”。
我的态度是:备库UNDO表空间可以适度扩大,作为常规容量管理的一部分,但不要指望它能根治01555。真正有效的方案,得从两条线一起走:一条是降低备库查询对UNDO的依赖,另一条是让UNDO覆盖行为变得可预测、可错峰。
5. 终极方案:从19c的临时UNDO白盒机制角度重构查询路径
5.1 备库查询的根本出路:Temporary UNDO(临时UNDO)
Oracle从12c开始引入了Temporary UNDO的概念,目的是在备库和使用了In-Memory Column Store的实例上,让查询过程中的临时数据(例如排序、hash join产生的临时结果)使用临时表空间而不是常规UNDO表空间。
到了18c/19c,这个能力被进一步完善。通过启用临时UNDO,ADG备库上的查询在需要UNDO进行一致性读时,会优先使用临时表空间中的临时UNDO段,而不是共享的UNDO表空间。这样查询的大部分UNDO处理就被“隔离”到了临时表空间里,不再跟主库同步过来的UNDO活动争抢空间。
这会带来两个好处:
- 查询的一致性读过程不再受主库UNDO覆盖节奏影响,备库能本地管理自己的临时UNDO生命周期。
- 即使主库出现了UNDO覆盖风暴,备库的长查询也能通过临时UNDO完成回滚构造,01555的概率大幅降低。
临时UNDO对ADG备库查询来说,更像是一个“本地避风港”,把备库查询的一致性读行为从主库的UNDO循环里解放出来。
5.2 启用临时UNDO的三个关键参数与配置流程
要在ADG备库上启用临时UNDO,需要配置以下参数:
sql复制-- 在备库上执行
ALTER SYSTEM SET TEMP_UNDO_ENABLED = TRUE;
这个操作在备库处于只读打开状态时也能执行,它是一个动态参数,不需要重启。执行后,新建立的会话会自动使用临时UNDO。
同时需要确保备库有足够的临时表空间。临时UNDO段是放在临时表空间中的,如果临时表空间空间太小,或者没有足够的临时文件,启用临时UNDO后反而可能报临时表空间不足的问题。
sql复制-- 查看当前临时表空间大小
SELECT tablespace_name,
file_name,
bytes / 1024 / 1024 AS size_mb,
maxbytes / 1024 / 1024 AS max_mb
FROM dba_temp_files;
-- 如果偏小,可以增加临时文件
ALTER TABLESPACE temp ADD TEMPFILE '/u01/app/oracle/oradata/ORCL/temp02.dbf'
SIZE 8G AUTOEXTEND ON MAXSIZE 32G;
第三个需要注意的参数是TEMP_UNDO_RETENTION,它控制临时UNDO在临时表空间中保留的时间:
sql复制ALTER SYSTEM SET TEMP_UNDO_RETENTION = 900;
这个参数默认值在不同版本中不一样,我建议根据你业务中最长查询的耗时来设置,比如查询最长跑30分钟,那这个值可以设置在1800到2400秒之间,给长查询留出足够的缓冲。
5.3 临时UNDO的限制:不是所有SQL都能受益
在把这个方案应用到生产环境之前,有一个必须知道的边界:并不是所有查询都能使用临时UNDO。
临时UNDO只对“纯只读查询”生效,具体来说,是那些不会产生任何数据库更改的SELECT语句。如果备库上运行的语句中包含任何写操作——哪怕是往全局临时表里插入数据,都会使查询退出临时UNDO的适用范围,回到传统的常规UNDO路径上,01555的梦魇就还会回来。
所以,如果你想通过临时UNDO解决备库上的01555,必须在应用侧同步确认一个前提:备库上跑的任务必须严格只读。不能有“先insert into某些日志表再查询”这种伪只读逻辑。
Oracle官方文档中也明确指出,Temporary UNDO在active Data Guard备库上的设计目标,就是支持长时间运行的只读查询。所以如果备库上需要运行的标准查询是真正的SELECT,那这个方案几乎是为这个场景量身定制的。
5.4 一个关键误区:会话级临时UNDO是什么时候生效的
另一个常见坑点是:启用TEMP_UNDO_ENABLED=TRUE后,正在运行的现有长查询不会自动切换到临时UNDO。临时UNDO的选择是在会话开始执行第一条语句时确定的。
也就是说,如果你在上午10点把一个长查询放出去了,10点10分才执行ALTER SYSTEM SET TEMP_UNDO_ENABLED=TRUE,那10点那个查询已经选定了传统的UNDO路径,它无法受益于临时UNDO。这个设置只对10点10分之后新建的会话或新提交的语句生效。
因此在实施切换时,务必和应用方协调好,让所有备库查询会话断开重连,或者等当前查询跑完后重新执行,否则会出现“明明启用了还是报01555”的假象。
6. 从参数优化到工程治理:构建抗01555的完整链路
6.1 主库UNDO配置优化:从源头控制覆盖节奏
备库的01555有相当比例是主库UNDO压力传导导致的,所以在备库做优化的同时,主库的UNDO配置也必须同步审视。主库的优化目标是让UNDO的覆盖行为尽量平滑,避免短时间内的“洪峰式覆盖”。
主库的UNDO_RETENTION设置不能一概而论,要根据业务特征来。如果主库有夜间批量任务,批量期间产生大量UNDO并快速覆盖,那么白天跑在备库的长查询就很容易中招。遇到这种情况,可以把主库的UNDO_RETENTION适当调大,比如从900秒调到1800秒甚至2400秒,给备库的长查询争取更多的缓冲时间。同时需要保证UNDO表空间容量与这个RETENTION设置匹配。
这里有一个计算公式可以参考:UNDO表空间最小容量 ≈ UNDO每秒产生速率 × UNDO_RETENTION秒数。
举个例子:一个系统高峰期的UNDO产生速率约为3MB/s。如果设置UNDO_RETENTION=1800秒,那么理论上需要的UNDO表空间至少是3MB × 1800 = 5400MB。当然这只是理想值,实际还要额外预留30%-50%的余量,应对并发波动。你可以在主库上用以下SQL查询实际的UNDO产生速率,然后倒推合适的UNDO表空间大小:
sql复制SELECT SUM(undoblks) * TO_NUMBER(
SUBSTR('8192', 1, INSTR('8192', ' ') - 1)
) / 1024 / 1024 / 600 AS undo_mb_per_sec
FROM v$undostat
WHERE begin_time > SYSDATE - 1;
6.2 备库的UNDO_RETENTION到底该怎么设置
在启用临时UNDO之前,备库的常规UNDO依然要配置合理。这里我的经验是:备库的UNDO_RETENTION可以设置得比主库更大一些,但不能无脑调大。
因为备库的UNDO段容量有限,设置过大的RETENTION会导致UNDO段无法及时清理,进而让UNDO表空间使用率持续走高。在ADG环境下,UNDO表空间增长意味着主库日志应用时的活动会更多,长期来看会增加MRP的I/O压力和备库的存储压力。
一个相对合理的配置原则是:备库的常规UNDO_RETENTION设置为你业务中典型长查询耗时的1.5倍到2倍。如果最长的报表要跑30分钟,那备库的UNDO_RETENTION可以设为2700到3600秒。再配合临时UNDO,双管齐下,覆盖问题的防护才完整。
备库上修改UNDO_RETENTION和RETENTION GUARANTEE要非常小心,有些版本可能需要重启备库才能生效。如果修改失败,检查是否因为当前备库正处于日志应用状态,对参数修改有额外限制。这里我建议先确认版本和当前状态再动参数:
sql复制-- 查看备库当前状态和日志应用情况
SELECT open_mode, database_role, guard_status
FROM v$database;
SELECT process, status, thread#
FROM v$managed_standby
WHERE process LIKE 'MRP%';
6.3 使用隐含事件延缓备库UNDO段清除(兜底方案)
再分享一个偏门但实用的兜底手段。如果生产环境出于种种原因还无法升级到19c或启用临时UNDO,或者临时UNDO也无法覆盖所有场景,可以尝试通过设置Oracle的隐含事件,让备库延迟UNDO段的清除。
在备库上执行:
sql复制ALTER SYSTEM SET EVENTS '10511 trace name context forever, level 1';
事件10511的作用是禁用UNDO段的事务表延迟清除(deferred cleanup)。设置后,备库上的UNDO段在事务提交后不会立即被标记为可覆盖,而是会多保留一段时间,从而给长查询留出更多的回滚窗口。
这个事件不是官方文档推荐的常规手段,但在某些特殊场景下确实能起到缓解作用。注意它不能替代UNDO_RETENTION的配置,也不能替代临时UNDO方案,它更像是在其他手段都用尽之后的“B计划”。使用前建议先在测试库上验证效果,同时在备库上观察日志应用和查询性能的表现。
6.4 报表类查询的重写与分片:从SQL层面降低UNDO需求
除了数据库层面的配置调整,SQL层面的优化同样重要。很多时候备库的长查询之所以容易撞上01555,核心原因是查询本身要访问的数据量太大,执行时间太长,导致它需要回看很长时间窗口内的一致性版本。
对这类SQL,我通常建议做三件事:
- 通过物化视图或汇总表预计算,把复杂的多表关联查询转换成对预计算结果的小查询,缩短执行时间。
- 在SQL中绑定正确的SCN或时间范围,如果业务允许查询“某个时间点的数据”,可以使用AS OF TIMESTAMP或闪回查询语法,让数据定位更加精确,减少对UNDO的依赖。
- 对超大批量查询做分片处理,把一次全量扫描拆成按时间分区或按主键范围的多批次小查询,每批查询执行时间控制在几分钟内,从而大幅降低撞上UNDO覆盖的概率。
这些SQL层面的优化,往往比UNDO参数调整更直接地消除01555的触发条件。
7. 问题排查与监控:建一条自动化的01555预警通道
7.1 快速定位:一条SQL判断01555的UNDO压力来源
当01555发生时,DBA需要第一时间判断压力是来自主库的批量事务,还是备库自身的超长查询。我常用下面的SQL在备库上做定向分析:
sql复制-- 查看过去一天UNDO产生的高峰期
SELECT TO_CHAR(begin_time, 'MMDD HH24:MI') AS time_slot,
ROUND(SUM(undoblks), 2) AS total_undoblks,
ROUND(SUM(txncount), 2) AS total_txns,
MAX(maxquerylen) AS max_query_len_sec
FROM v$undostat
WHERE begin_time > SYSDATE - 1
GROUP BY TO_CHAR(begin_time, 'MMDD HH24:MI')
ORDER BY total_undoblks DESC;
如果结果中某个时间段的TOTAL_UNDOBLKS极高,同时备库Alert日志中的01555报错时间正好落在这个高峰之后,那么基本可以判断是UNDO覆盖传导。反之,如果UNDO产生量不高,但01555依然出现,那更可能是单个查询执行时间过长,需要重点排查SQL本身的执行计划是否存在严重的笛卡尔连接或缺失谓词的问题。
7.2 长期监控:把01555消灭在发生之前的预警机制
01555这类问题,最好的处理方式是在它发生之前提前预警。常规的监控阈值里,大多数人只看UNDO表空间使用率,但01555跟表空间使用率没有必然联系,更关键的是“UNDO覆盖速率”和“最长查询时长”之间的关系。
我建议在监控系统里增加以下两个指标:
v$undostat.MAXQUERYLEN的最大值,如果这个值逼近或超过了UNDO_RETENTION,说明系统内存在超长查询,01555风险显著上升。v$undostat.TUNED_UNDORETENTION的值,这是Oracle根据UNDO压力自动计算出的一个推荐保留时间。如果TUNED_UNDORETENTION持续大于你实际设置的UNDO_RETENTION,系统就是在告诉你“当前的UNDO保留设置不足以支撑查询需求”。
这两个指标可以做成一个简单的查询脚本,定期跑一次并输出告警:
sql复制SELECT TO_CHAR(SYSDATE, 'MMDD HH24:MI:SS') AS check_time,
MAX(maxquerylen) AS max_query_seconds,
MAX(tuned_undoretention) AS suggested_retention,
MAX(undoblks) AS peak_undoblks
FROM v$undostat
WHERE begin_time > SYSDATE - 1 / 24;
当SUGGESTED_RETENTION连续3次(比如间隔5分钟一次)大于当前配置的UNDO_RETENTION时,就触发告警,提醒DBA去确认是否有长查询在备库上运行,提前介入。
7.3 备库01555发生后的标准检查清单
最后把01555发生后的标准操作顺序整理成一张清单,方便直接参考:
| 排查项 | 执行内容 | 判定标准 |
|---|---|---|
| 确认报错位置 | 查询备库Alert日志中报错时间点的trace文件 | 报错伴随kdifind信息,确认备库本地 |
| 查主导SQL | 找到报错会话的SQL_ID和SQL文本 | 确认执行时长超过UNDO_RETENTION |
| 查UNDO高峰 | 查v$undostat过去24小时数据 | 定位UNDO峰值时间段 |
| 核对主库操作 | 查主库定时任务与批量作业时间 | UNDO高峰是否与批量任务重叠 |
| 查UNDO配置 | 检查UNDO表空间AUTOEXTEND和大小 | 确认空间是否足够、可扩展 |
| 应用侧调整 | 确认备库查询是否严格只读 | 若有写操作,临时UNDO方案无效 |
| 启用临时UNDO | 设置TEMP_UNDO_ENABLED=TRUE | 新会话自动使用临时UNDO |
这张表既是排查流程,也是我在给客户做巡检时的标准动作清单。顺手把结果记录到运维文档里,后续再出现同类问题时,可以直接对照历史记录快速找到变化点。
8. 实战设置建议:一套可直接落地的参数组合
8.1 不同版本下临时UNDO的兼容性核查
虽然临时UNDO在多版本中都有提及,但真正能放心用在生产环境ADG备库上,建议至少使用18c,更推荐19c及以上版本。
在18c中,临时UNDO默认是关闭状态,需要显式启用。而19c中对备库使用临时UNDO的限制已经比较少,文档也明确支持。20c之后的版本(包括21c)对备库的支持就更完整了,但生产环境用21c的还不多,这里就不展开。
启动前的一个基础检查是在备库上确认COMPATIBLE参数不低于18.0:
sql复制SHOW PARAMETER COMPATIBLE;
SHOW PARAMETER TEMP_UNDO_ENABLED;
如果COMPATIBLE小于18.0,即使你执行了ALTER SYSTEM SET TEMP_UNDO_ENABLED=TRUE,Oracle也不会真正启用临时UNDO。这是一个很多人翻车的地方。
8.2 最小化改造方案:只针对备库查询会话启用临时UNDO
如果你不希望全局启用临时UNDO(比如主库也需要跑一些特殊的任务,担心受到影响),可以在备库上通过ALTER SESSION的方式,只对特定会话启用临时UNDO:
sql复制-- 在备库的会话中执行
ALTER SESSION SET TEMP_UNDO_ENABLED = TRUE;
这种方式非常灵活,适合那些只在特定时间段运行的报表任务。只需在报表连接池的数据库连接初始化脚本里加上这一行,就能让这个连接池的所有查询受益于临时UNDO,而对其他会话完全无感。
不过有个细节要注意:ALTER SESSION方式需要建立在“会话创建以后、第一个查询开始以前”这个时间窗口内。如果连接池已经建立并复用了很久的会话,那么需要在连接池将所有连接重建之后,ALTER SESSION才能在其后执行且生效。
8.3 推荐的UNDO与临时空间参数基线
基于我处理多个生产项目的经验,这里给出一套比较稳妥的参数基线,你可以根据自己的硬件配置和业务特点做缩放:
- 主库UNDO_RETENTION: 1500~2400秒(根据批量任务高峰调整)
- 备库常规UNDO_RETENTION: 2400~3600秒
- 备库TEMP_UNDO_ENABLED: TRUE
- 备库TEMP_UNDO_RETENTION: 1800~2400秒
- UNDO表空间自动扩展: 开启,MAXSIZE按实际容量1.5倍预留
- 临时表空间: 至少为UNDO表空间大小的50%以上
这套配方的思路是:常规UNDO应对主库同步过来的历史覆盖需求,临时UNDO应对备库本地长查询的读一致性需求,两个路径分开管理,可以最大化降低它们互相干扰的概率。
8.4 变更后的验证方法
所有参数修改完成后,不要急着下结论说问题解决了,要做一轮完整的验证。我通常会在备库上这样做:
sql复制-- 1. 确认会话的临时UNDO状态
SELECT SYS_CONTEXT('USERENV', 'TEMP_UNDO_ENABLED') AS temp_undo_status
FROM dual;
-- 2. 构造一个耗时较长的查询,观察v$temp_undo_stat是否生成记录
SELECT SUM(active_undo_size / 1024 / 1024) AS temp_undo_mb
FROM v$temp_undo_stat;
如果查询执行期间,v$temp_undo_stat中有数据了,说明临时UNDO机制已经生效。另外建议在备库上主动跑一次之前01555报错的SQL,看看执行计划和性能是否有明显变化,一旦确认无异常,再把监控告警阈值设置好。
注意:启用临时UNDO后,备库上某些依赖UNDO的会话行为会发生细微变化,例如闪回查询的AS OF TIMESTAMP可能在使用临时UNDO时受到限制。如果备库上有依赖闪回查询的业务,需要提前测试确认兼容性。
9. 远期规划:通过ADG备库的读拆分消解长查询压力
9.1 多备库架构的角色分工
如果一套ADG环境里,备库既要承载实时报表,又要承载月末对账、数据抽取等长任务,那么01555几乎是不可避免的。不同类型的查询,对UNDO和数据库资源的需求差异极大,硬塞在同一个备库实例上,互相挤兑是常态。
一个更彻底的工程解法是引入多备库架构,让不同类型的查询跑在不同备库上。比如:
- 主库负责OLTP写入;
- 备库A负责实时的短查询、在线业务查询,这类查询执行时间短,不容易撞上UNDO覆盖;
- 备库B专门承载批处理、数据抽取、复杂报表,这类查询执行时间长,我们把它的UNDO_RETENTION调到最大,并启用临时UNDO,让它成为一个完全面向长查询的“分析型备库”。
这种方式对业务改造几乎为零,成本主要在硬件投入上。对一个承载关键业务的大型系统来说,用一台备库的硬件成本换取业务稳定性,是完全划算的。
9.2 备库查询与主库日志应用的时间窗口错峰
还有一个比较巧妙但有效的思路是:在主库批量作业开始前,让备库的长查询提前结束,错开UNDO覆盖的高峰窗口。
这听起来像是一句废话,但很多系统没有做到位。主库批量任务是有明确时间点的,比如每天下午14:00启动。备库的报表调度完全可以避开这个时间窗口,把重要报表安排在13:00之前跑完,或者推迟到15:30之后再启动。通过简单的调度配置,就能显著减少备库长查询与主库UNDO高峰重叠的概率。
这个方案不需要改任何数据库参数,只调整应用调度策略,是最省力但也最容易被忽视的优化空间。
9.3 升级迁移到Oracle 19c+的收益评估
如果当前系统还在11g或12c的低版本上,且ADG备库频繁出现01555,那么升级到19c是值得认真评估的选项。19c不仅是临时UNDO功能完善度最高的版本之一,还引入了很多针对ADG只读场景的优化,对备库类查询的稳定性有明显改善。
升级工作本身也是个工程,需要考虑停机窗口、回退预案和应用兼容性。但从01555这个故障的长期治理角度看,升级带来的系统性收益远大于继续在旧版本上打补丁、调参数。
10. 写在最后的一点经验和建议
回溯这些年在ADG备库上排查ORA-01555的经历,我最大的感受是:这个错误的表象非常单纯,但根因往往埋在好几层之外。如果你只盯着备库的UNDO表空间和UNDO_RETENTION,那大概率会陷入“调了参数没效果、扩了空间还报错”的循环里。
真正有效的思路,是先分清备库的UNDO压力到底来自主库还是本地查询,然后用临时UNDO把两条路径解耦,再配合SQL治理和调度策略优化,把01555的触发条件逐一拿掉。
最后分享一个小经验:主库与备库版本的兼容性,在排查这类问题时经常被忽略。如果主库是19c但备库还停留在旧版本,临时UNDO的部分特性可能无法完整生效。我在实践中习惯在每次补丁升级后,都在备库上重新执行一遍SHOW PARAMETER TEMP_UNDO_ENABLED和SELECT * FROM v$temp_undo_stat做验证。安全无小事,尤其在数据保护这种关键链路上,每次变更后的确认动作都值得多做一步。
