上个月,一个朋友发了个 Oracle 19c 库的 expdp 导出文件给我,让我帮忙灌到测试环境。文件 20 多个 G,我心想半小时应该能导完,结果 impdp 跑了不到三分钟就直接摔了一个 ORA-39405。原因很明确:源库时区版本已经升到 42,而我测试库的 Oracle 19c 还停在默认的 32。TSTZ 报错这个事,遇过一次就明白,它跟数据泵本身没关系,纯粹是源端和目标端的时区文件版本对不上。这篇文章就把我从 32 升到 42 的完整过程、数据泵报错的排查链路、以及几个值得写进运维手册的坑全部摊开讲。
1. 时区版本为什么会从 32 升到 42:先说清楚报错的根源
1.1 数据泵 TSTZ 报错的第一现场
先描述一下我遇到的现场。impdp 报错大概长这样:
ORA-39405: 从源数据库导出的数据中包含比目标数据库更高版本的时区数据这个报错说明源端导出文件里的 TIMESTAMP WITH TIME ZONE 列,使用的是比目标库更“新”的时区规则。Oracle 内部每个时区文件都有版本号,比如 32、33、35、37、42。目标库只有 32,而 DMP 文件里带有 42 的时区数据格式,数据泵不认识,干脆拒绝干活。
还有另一种常见情况,expdp 导出的时候就会带警告:
WARNING: ORA-39405: Oracle Data Pump Export file contains data with timestamp version higher than the target这种情况通常发生在源库已经升级过时区版本,而目标库还是旧版本。导出的 DMP 本身没问题,但导入端版本不够,就会报错。
1.2 Oracle 时区版本号的迭代逻辑
很多人第一次看到时区版本号会懵,32 和 42 到底代表什么?简单说,这是 Oracle 对全球时区规则变更的版本管理。不同国家的夏令时调整、时区归属变更、政治边界变化,都会影响时间戳的偏移量计算。Oracle 通过发布新的时区文件,把这些规则更新固化进去。
每次时区文件更新,版本号就会递增。比如某个国家在 2023 年取消了夏令时,旧文件里没有这条规则,导致数据库转换出来的本地时间不对,那就需要升级时区文件。19c 刚安装完,默认时区版本一般是 32。如果后面打了较新的 RU 补丁,$ORACLE_HOME/oracore/zoneinfo 目录下可能会多出更高版本的时区文件。
版本号 42 属于比较新的状态。从 32 一步跨到 42,意味着中间所有时区规则变更全部生效。Oracle 支持一次性升级到数据库软件所带的最高时区版本,不需要按照 32→33→34 这样逐级升。这是很多 DBA 的误区,总以为像补丁一样要逐级打,其实不是。
1.3 什么场景下才真的需要做时区升级
不是所有环境都需要升级到 42。我建议你先问一个问题:业务数据里有没有 TIMESTAMP WITH TIME ZONE 或 TIMESTAMP WITH LOCAL TIME ZONE 类型的列?如果没有,可以不用折腾。
如果有,再看第二个问题:业务方是否明确提出了新时区规则的需求,或者你在导入增量数据时已经遇到了 ORA-39405。只有满足这些条件,才值得做。纯粹为了“新版更高级”去升级时区,没必要,升级过程会锁表、产生大量 undo,对生产环境有风险。
我当时之所以升级,是因为源生产库已经升级到了 42,测试环境需要从生产库拿 DMP 做功能验证,不升的话所有数据泵导入都会卡死在 ORA-39405 上。
2. 升级前倒查三件事:现状、影响面、回滚条件
2.1 先确认当前版本,以及软件是否支持升到 42
升级前第一件事,是确认当前时区版本和目标版本。用 SYS 用户连进数据库:
SQL> SELECT * FROM v$timezone_file; VERSION ---------- 32这是数据库当前使用的时区版本。接下来确认软件自带的时区文件版本,Linux 下看这个目录:
[oracle@db01 ~]$ ls -l $ORACLE_HOME/oracore/zoneinfo/如果看到 timezlrg_42.dat 或者 timezlrg_42.dat.Z,说明二进制支持升级到 42。Windows 环境对应路径是%ORACLE_HOME%\oracore\zoneinfo,同样检查有没有对应版本的文件。
这里有一个判断技巧:不要把目光只盯在 42 上。软件支持哪个版本,就以哪个版本为目标。如果你的 ORACLE_HOME 里最高只有 38,那目标就是 38,别想着一步到 42,除非先给 ORACLE_HOME 打补丁。
2.2 找到所有受影响的 TSTZ 表和列
升级时区版本不是改一个参数就完事,数据库里所有 TSTZ 类型的数据都要跟着转换一遍。所以升级前必须知道哪些表、哪些列受到影响。我习惯用这条 SQL 查全库:
SELECT owner, table_name, column_name, data_type FROM dba_tab_columns WHERE data_type IN ('TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE') ORDER BY owner, table_name, column_name;也可以直接查官方视图:
SELECT * FROM dba_tstz_tables;dba_tstz_tables 结构更清晰,能看到 owner、table_name、table_type 这些信息。查出来的结果发给业务方确认一遍,有些表可能是历史遗留,平时根本不读写,但你不知道业务隐含依赖,最好让他们过目。
另外要注意,物化视图、IOT 表、分区表也在处理范围内,不要只盯着普通堆表。分区表的 TSTZ 列更新时,涉及分区裁剪和全局索引维护,执行时间会比普通表长不少,后面做时间估算时要把这部分算进去。
2.3 备份、undo 空间和窗口设计
升级前一定要做备份。DBMS_DST 虽然提供了 ABORT_UPGRADE 的回退机制,但那只保证你回到 Begin_Upgrade 之前的状态。如果升级过程中数据字典已经发生了部分变更,回退也会很麻烦。最稳妥的做法是 RMAN 全备:
RMAN> BACKUP DATABASE PLUS ARCHIVELOG;对于关键业务库,如果条件允许,最好做一次克隆环境演练,先在克隆库上完整走一遍升级流程,记录耗时和报错,再回生产操作。
UNDO 表空间是这次升级里最容易被低估的资源。UPDATE_TSTZ_TABLE 每次调用都会产生大量 undo,一次升级下来,undo 使用量可能达到几十 GB。建议升级前检查 undo 表空间剩余空间:
SELECT tablespace_name, sum(bytes)/1024/1024 AS size_mb, sum(maxbytes)/1024/1024 AS max_mb FROM dba_data_files WHERE tablespace_name = 'UNDOTBS1' GROUP BY tablespace_name;如果剩余空间不够,直接加数据文件,并设置自动扩展:
ALTER TABLESPACE undotbs1 ADD DATAFILE '/u01/app/oracle/oradata/ORCL/undotbs02.dbf' SIZE 20G AUTOEXTEND ON NEXT 1G MAXSIZE 50G;还有一个很容易忽略的点:升级过程建议放在维护窗口,并且通知业务停掉针对 TSTZ 表的写入。虽然不要求停库,但业务 DML 会和 UPDATE_TSTZ_TABLE 产生锁竞争,严重时会拖垮整个库。
3. 用 DBMS_DST 完成 32 到 42 的升级实操
3.1 一次顺利的最小化升级过程
我用 SYS 用户执行,整个过程分五步。第一步,开始升级:
SQL> EXEC DBMS_DST.BEGIN_UPGRADE(TRUE);BEGIN_UPGRADE 会立即将数据库的时区版本切换到目标版本,同时创建一个升级状态标记。执行完立刻查一下:
SQL> SELECT * FROM v$timezone_file; VERSION ---------- 42注意,此刻数据库版本号已经变了,但数据字典中的时区数据还没更新。第二步,加载升级需要的时区表数据:
SQL> EXEC DBMS_DST.LOAD_UPGRADE_TABLE;这一步是把新版本时区文件中的规则加载到系统表里。官方有些文档写的是 LOAD_UPGRADE_TABLE,个别版本参数名有差异,如果不确定,先执行DESC DBMS_DST查看包内过程签名。
第三步,更新所有业务表上的 TSTZ 列。这是整个升级的核心耗时段。先对一张表手动调用:
SET SERVEROUTPUT ON DECLARE v_affected NUMBER; v_error_cnt NUMBER; v_failed NUMBER; BEGIN DBMS_DST.UPDATE_TSTZ_TABLE( ownername => 'SCOTT', tablename => 'ORDERS', columnname => 'ORDER_TIME', affected_rows => v_affected, error_count => v_error_cnt, failed => v_failed ); DBMS_OUTPUT.PUT_LINE('affected=' || v_affected || ' errors=' || v_error_cnt || ' failed=' || v_failed); END; /重要细节:UPDATE_TSTZ_TABLE 一次未必能更新完所有行,需要循环调用,直到 affected_rows 等于 0。所以不要只执行一次就急着做下一步。我当时用了一段循环脚本,把前面 dba_tab_columns 查出来的所有 TSTZ 列都处理了一遍,脚本大体是这个结构:
SET SERVEROUTPUT ON DECLARE v_affected NUMBER; v_error_cnt NUMBER; v_failed NUMBER; BEGIN FOR t IN (SELECT owner, table_name, column_name FROM dba_tab_columns WHERE data_type IN ('TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE') AND owner NOT IN ('SYS','SYSTEM') ORDER BY owner, table_name, column_name) LOOP DBMS_OUTPUT.PUT_LINE('Processing: ' || t.owner || '.' || t.table_name || '.' || t.column_name); LOOP BEGIN DBMS_DST.UPDATE_TSTZ_TABLE( t.owner, t.table_name, t.column_name, v_affected, v_error_cnt, v_failed); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('ERROR: ' || SQLERRM); v_affected := 0; END; DBMS_OUTPUT.PUT_LINE(' affected=' || v_affected || ' errors=' || v_error_cnt || ' failed=' || v_failed); EXIT WHEN v_affected = 0 OR v_affected IS NULL; END LOOP; END LOOP; END; /第四步,处理升级过程中产生的错误行。升级时如果某些行的时区值在新版本规则下变成非法,它们会被记录到SYS.DST$ERROR_TABLE。检查这张表:
SELECT * FROM sys.dst$error_table;如果有数据,必须手工修正这些行的时区值,然后重新对相应表执行 UPDATE_TSTZ_TABLE,直到错误表为空。这一步不能跳过,否则第五步会失败。
第五步,结束升级:
SQL> EXEC DBMS_DST.END_UPGRADE;再确认一下最终状态:
SQL> SELECT version FROM v$timezone_file; VERSION ---------- 42 SQL> SELECT property_value FROM database_properties WHERE property_name = 'DST_UPGRADE_STATE'; PROPERTY_VALUE ---------------- NONE看到 DST_UPGRADE_STATE 是 NONE,才算真正完成。
3.2 UPDATE_TSTZ_TABLE 到底在做什么
这一步想解释一下 UPDATE_TSTZ_TABLE 的内部逻辑,理解它才能写出靠谱的循环脚本。
TSTZ 列存储的其实是 UTC 时间加上时区区域 ID。旧的时区文件里,某个时区区域对应的规则可能是 UTC+8,新文件里可能变成了 UTC+9。UPDATE_TSTZ_TABLE 的作用就是把这些行的时区偏移量从旧规则重算成新规则。
每次调用,Oracle 会扫描全表,找到需要更新的行,但为了控制 undo 和 redo 压力,不会一次性把一百万行全部改完,而是分批次处理。所以才会要求重复调用,直到 affected=0。这个设计和普通 UPDATE 的批提交逻辑很像,你不要指望一次调用就清理干净,尤其大表。
还有一点,UPDATE_TSTZ_TABLE 内部会产生大量 DML 和索引维护,尤其是表上有多个全局索引时,耗时会被放大。升级前最好把大表的索引策略跟业务确认一下,如果允许,可以考虑先删掉部分次要索引,更新完再重建。我这次没删索引,结果一张 8000 万行的分区表跑了将近四十分钟,后来想想,如果事先把非必要索引收掉,时间能省不少。
3.3 升级中途失败怎么止损
最常遇到的失败场景是第 3 步,也就是 UPDATE_TSTZ_TABLE 跑到一半,UNDO 表空间满了,整个会话报 ORA-30036。这时不要慌,先处理空间,再继续执行,不用从头来。
如果实在想中止整个升级,Oracle 提供了 ABORT_UPGRADE:
SQL> EXEC DBMS_DST.ABORT_UPGRADE;这个操作会把数据库时区版本回退到升级前。但要注意,如果在 BEGIN_UPGRADE 之后你已经手动执行过大量数据变更,ABORT_UPGRADE 只能把系统表状态回退,业务表数据的物理修改不会自动回滚,只能依赖 undo 或备份恢复。所以我的建议是:一旦发现问题,第一选择永远是扩容 undo 继续跑,而不是轻易 ABORT。
还有一个小坑,升级过程中如果数据库实例异常重启,重启后会处于“升级未完成”的中间状态。连接数据库执行 DDL 可能会报 ORA-30079 之类,提示数据库正在进行时区升级,不能执行该操作。这时候不要乱动,先检查 DST_UPGRADE_STATE,根据状态决定是继续跑完还是 ABORT。
4. 数据泵 TSTZ 报错的完整排查链路
4.1 读报错日志,分清“版本不匹配”和“数据非法”
拿到一个 DMP 文件导入报 ORA-39405,很多人第一反应是检查 DMP 文件损坏没有、磁盘空间够不够,方向全偏了。正确做法是先看完整报错上下文,区分两类问题。一类是 ORA-39405 版本不匹配,另一类是 ORA-01882 时区区域找不到,后者说明 DMP 文件里带了一个目标库时区文件里根本不存在的时区名,比如业务数据里写了一个旧版本支持、新版本已经删除的区域。
区分方法很简单:在目标库执行:
SQL> SELECT * FROM v$timezone_file;查完当前目标版本后,再看源库的版本。如果源库更高,就是版本不匹配,走升级路线;如果版本一样但还报错,重点查 DMP 里涉及的具体时区区域,可以用 impdp 的 SQLFILE 参数先只生成 SQL,不实际导入:
impdp directory=DUMP_DIR dumpfile=full.dmp logfile=impdp_check.log sqlfile=import.sql这样能把 DDL 和部分数据读取过程触发的问题暴露出来,而不会真正污染数据库。
4.2 目标库升级后用 expdp/impdp 重新打通
一旦确认是目标库版本太低,标准解法就是升级目标库时区版本,具体流程按第 3 章走。升完目标库后,重新导入同一个 DMP 文件,之前卡住的 ORA-39405 会直接消失。
这里多说一句,升级完目标库后,建议顺手做一次完整的 expdp/impdp 往返测试,确认不是“碰巧能导”而是“真正没问题”。我当时是先导一个小用户验证,再导全库。全库导入时把并行度调低一点,避免因为 IO 压力产生其他误导性报错:
impdp directory=DUMP_DIR dumpfile=full.dmp logfile=impdp_full.log parallel=4 exclude=STATISTICS4.3 实在不能升级目标库时的几条旁路
有些环境确实动不了目标库,比如第三方托管的库、或业务不同意任何维护窗口。这种情况下,能走的路有两条,但都有代价。
一条是在源库把 TSTZ 列转换成普通 TIMESTAMP 或 VARCHAR2 后再导出。这要求你有源库的访问权限,而且判断业务是否可以接受数据语义的变化。比如一个订单创建时间列,如果业务只关心本地时间而不关心时区转换,可以临时用 CAST 处理。但这样做的问题很明显:批量操作大表时,源库压力很大,而且如果你拿到的 DMP 是别人给你的,这条路根本走不通。
另一条是在源库把时区版本临时降级到与目标库一致再导出。时区版本不是不能降,但这是一条高风险路线,需要对所有 TSTZ 表做一遍反向数据修正,任何遗漏都会造成时间偏移错误,我强烈不建议在生产环境使用。
所以结论很直接:ORA-39405 面前,升级目标库是唯一干净的答案,旁路只是临时救火手段。
5. 值得记进运维手册的几个坑
5.1 UNDO 和临时表空间准备不足的后果
前面说过 UNDO 很容易爆,再补充一个临时表空间的问题。升级过程中,Oracle 会对 DBA_TSTZ_TABLES 里涉及的表做全表扫描和排序操作,临时表空间使用量也会上去。如果临时表空间是固定大小且已经被别的排序操作占满,升级会话会直接报 ORA-01652。
所以升级前这两处检查一个都不能少:
SELECT tablespace_name, sum(bytes)/1024/1024 AS size_mb FROM dba_temp_files GROUP BY tablespace_name;如果临时表空间不够,可以通过增加临时文件扩容:
ALTER TABLESPACE temp ADD TEMPFILE '/u01/app/oracle/oradata/ORCL/temp02.dbf' SIZE 10G;5.2 业务表更新遗漏,后续写入直接报错
升级脚本如果只处理了 SYS/SYSTEM 之外的表,或者有些表在应用运行时才被动态创建,那么 END_UPGRADE 之后,新插入的 TSTZ 数据可能仍然按照旧规则解析,轻则数据不一致,重则直接报 ORA-01882。
这个坑我踩过一次,原因是升级前查 DBA_TAB_COLUMNS 时,一个应用启动后才建的表没有被覆盖到。后来靠定时任务每天晚上巡检 TSTZ 表清单才兜住。建议把升级巡检脚本保留下来,每个月跑一次:
SELECT owner, table_name, column_name, data_type FROM dba_tab_columns WHERE data_type IN ('TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE') AND owner NOT IN ('SYS','SYSTEM') ORDER BY owner, table_name;5.3 时区升级与 RAC/DataGuard 的执行顺序
RAC 环境下,DBMS_DST 只需要在一个实例上执行,但其他实例不要同时跑业务 DDL,避免 lock 竞争。升级完成后,建议对所有节点做一次滚动重启,确保所有实例都加载了新的时区文件。
DataGuard 环境下要特别注意:备库不需要手工执行 DBMS_DST,但备库的 ORACLE_HOME 里必须预先放好目标版本的 timezlrg_42.dat 文件,否则主库升级后,redo 传到备库应用时,备库会因为找不到对应版本时区文件而中断恢复。正确顺序是先把时区文件同步到备库二进制目录,再在主库执行升级,最后观察备库 alert 日志确认 apply 正常。
5.4 升级后的验证清单
最后分享一个我每次升级完都会执行的验证清单,照着做一遍,心里才踏实:
- 查 V$TIMEZONE_FILE,版本为目标版本
- 查 DATABASE_PROPERTIES,DST_UPGRADE_STATE 为 NONE
- 查 SYS.DST$ERROR_TABLE,确保没有任何错误行
- 对每个 TSTZ 表再次执行 UPDATE_TSTZ_TABLE,affected 均为 0
- 插入一条携带具体时区区域的数据,例如
FROM_TZ(CAST(SYSDATE AS TIMESTAMP), 'Asia/Shanghai'),再查出来确认偏移量正确 - 做一次用户级 expdp 和 impdp 往返,确认 ORA-39405 不再出现
整个升级流程我用了一个常规维护窗口完成,最耗时的不是前几步,而是大业务表的 TSTZ 列批量更新。如果你手里也有一个 19c 库卡在 ORA-39405 上,别急着删表重导,先看一眼两边的时区版本,大多数情况下升完目标库就一切正常了。