☰
Oracle 19c数据泵导入ORA-39405:时区版本从32升到42完整指南
2026/10/14 23:55:22 网站建设 项目流程

简介:面向Oracle数据库管理员、DBA以及处理数据迁移的运维工程师,这套zip压缩包聚焦Oracle 19c时区版本从32升级至42的典型场景,重点用于解决数据泵(Data Pump)导出和导入过程中TSTZ(时区敏感时间戳)字段出现兼容性报错的问题。包内共10个文件,包含6个dat格式的时区数据库文件、2个xml配置文件和2个txt说明文档,整体约377KB,以极小体积覆盖了时区升级所需的时区标签数据、数据库核心配置与操作指引,适合快速部署与排查。目前已有1372人学习/下载,说明该主题在跨时区数据同步、灾备恢复等实际业务中具有较高关注度。借助包内的时区数据文件、配置示例与说明文档,用户不仅能理解时区版本差异如何影响TSTZ类型数据的读取,还能掌握升级前后对数据泵导入参数的相应调整方法,从而减少因时区库不一致导致的导入失败风险,适用于对Oracle多时区环境有严格要求的DBA与系统运维人员。

1. 时区版本从32升到42,数据泵TSTZ报错的根因

两套同样是 19c 的库,一套是刚装完基础环境、时区版本还停在 32;另一套是打了常规补丁、时区版本已经到 42。我用后者做数据泵导出,再往前者导入,任务跑不到一半就报ORA-39405,日志里明确写着目标库时区版本太低,不支持导入。这个报错既不是权限问题,也不是表空间不够,更不是 dump 文件损坏,而是 Oracle 在 dump 头里写死了源库当时的时区版本。数据泵导入端一旦发现自己的时区版本低于 dump 里的版本,会在加载数据前直接拒绝执行。标题里说的 32 升 42,就是先把目标库的时区版本提上去,让数据泵认为两边的时区规则处于同一代,然后再导数据。这篇文章就把检查、升级、验证和导入的完整链路讲清楚,适合正在做 19c 之间迁移、或者从更高补丁环境往新库补数据的从业者。

2. 升级前先看准:时区版本与数据泵报错的关系

2.1 一条 SQL 看清当前时区版本

排查这种事,第一步永远是先确认现状,而不是直接跑升级。我通常会在目标库执行下面这一组查询,把时区文件、数据库时区、会话时区和升级状态一次性拉出来。

-- 当前使用的时区文件与版本 SELECT FILENAME, VERSION FROM V$TIMEZONE_FILE; -- 数据库默认时区和会话时区 SELECT DBTIMEZONE, SESSIONTIMEZONE FROM DUAL; -- 当前数据库是否处于时区升级中 SELECT PROPERTY_VALUE FROM DATABASE_PROPERTIES WHERE PROPERTY_NAME = 'DST_UPGRADE_STATE'; -- 当前 Oracle Home 能支持的最新时区版本 SELECT DBMS_DST.GET_LATEST_TIMEZONE_VERSION FROM DUAL;

第一条查询里,FILENAME通常是timezlrg_32.dat,VERSION是 32,说明数据库当前启用的就是 32 版时区规则。第三条查询如果没有返回结果,或者状态是NONE,说明库里没有进行中的时区升级。最后一条函数很关键,它返回的是 Oracle Home 里时区数据文件实际支持的最高版本,不一定等于 42,可能还是 32。如果最后一条返回 42,则具备升级条件;如果返回 32,说明连软件层面的时区文件都不满足要求,后面升级会卡住。

很多人在这一步会混淆两个概念:DBTIMEZONE是数据库默认时区,比如Asia/Shanghai或+08:00;V$TIMEZONE_FILE.VERSION是时区规则文件的版本。前者表示“数据库按哪个时区解释时间”,后者表示“Oracle 用哪一年的夏令时规则去解释带时区的时间戳”。升级时区版本不会改变DBTIMEZONE,这一点后面排坑还会用到。

2.2 数据泵为什么会盯住时区版本

TSTZ全称是TIMESTAMP WITH TIME ZONE,它存储的不只是一个时间点,还带时区区域名,比如2025-06-01 12:00:00 Asia/Shanghai。时区区域名本身没有歧义,但区域在不同年份的夏令时偏移规则是会变的。Oracle 不可能把这些规则硬编码在每一个数据库实例里,而是统一收在timezlrg_*.dat文件里,每个文件对应一个版本号。版本 32 是老规则集,版本 42 则包含这些年各国调整过的夏令时和时区偏移规则。

数据泵导出时,会把当前源库的时区版本号写进 dump 文件的头部。导入时,impdp会先读这个版本号,然后跟目标库V$TIMEZONE_FILE.VERSION做比较。如果 dump 里的版本高于目标库,就说明目标库的时区规则太旧,无法正确解释 dump 内部可能出现的某些时区偏移。Oracle 的判断是“与其导进去之后时间算错,不如直接报错”,于是就有了ORA-39405。这个设计属于保护性机制,不是数据泵的 bug,所以靠改参数硬闯基本走不通。

这里有个常见的误操作值得单独说一句:有人尝试在expdp里加VERSION=19.3,想通过降低兼容版本来绕过检查,但VERSION参数控制的是 Oracle 数据库版本兼容性,并不是时区版本。dump 头里的时区版本标记不受它影响,目标库照样会拒绝。数据泵的字符集不匹配好歹还能通过转换解决,时区版本不一致没有类似开关。

2.3 升级前的备份与窗口判断

时区版本升级不是一条ALTER DATABASE就能搞定的事,它需要调用DBMS_DST包,并且升级过程中要重启两次数据库实例,所以必须按维护窗口来评估。我一般会按下面几步做前置准备。

先把带TIMESTAMP WITH TIME ZONE和TIMESTAMP WITH LOCAL TIME ZONE列的表找出来,估算升级时需要改写的数据量:

SELECT tc.TABLE_NAME, tm.NUM_ROWS FROM DBA_TAB_COLUMNS tc JOIN DBA_TABLES tm ON tc.OWNER = tm.OWNER AND tc.TABLE_NAME = tm.TABLE_NAME WHERE tc.DATA_TYPE IN ('TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE') ORDER BY tm.NUM_ROWS DESC;

这条 SQL 查的是普通表的估算行数,分区表需要单独统计,但它足够帮我们判断哪些表是这次升级的大头。时区升级会把涉及到的 TSTZ 和 LTZ 数据进行重写,行数越大,redo 和归档日志量就越大,升级窗口越长。

然后做三件事:第一,对目标库做一次完整备份,或者至少保证有可恢复的最近备份,这是“后悔药”。第二,确认 undo 表空间和归档目录有足够余量,升级过程中产生的归档日志往往比平时多出不少。第三,通知业务侧准备断连,因为流程里有两次SHUTDOWN IMMEDIATE,应用连接会被中断。如果库里存在上千万行的 TSTZ 表,我建议先在一套克隆库上把升级时间实测一遍,再排生产窗口。

3. 用时区升级包把数据库时区版本从32升到42

3.1 确认时区数据文件已支持42

在调用DBMS_DST之前,还需要确认 Oracle Home 里确实有 42 版的时区文件。最直接的办法是看zoneinfo目录:

ls -l $ORACLE_HOME/oracore/zoneinfo/timezlrg_*.dat

正常情况下能看到timezlrg_32.dat、timezlrg_42.dat等一系列文件。只有这些文件存在,DBMS_DST.GET_LATEST_TIMEZONE_VERSION才会返回 42,后续升级才有基础。

如果目录里只有timezlrg_32.dat,说明当前 Oracle Home 的软件补丁级别不够。常见做法是先把数据库软件升级到包含更高时区文件的补丁级别,再回来做时区版本升级。我不建议直接从别的环境复制.dat文件到当前 Oracle Home,因为时区文件和软件版本存在依赖关系,混合不同来源的文件容易留下隐患。先把软件层面补齐,数据库层面的时区版本升级就是水到渠成的事。

3.2 标准升级:三个命令加两次重启

确认文件没问题后,升级过程本身并不复杂,关键是顺序不能乱。整个流程可以概括为三句EXEC和两次SHUTDOWN IMMEDIATE; STARTUP。

第一步,执行BEGIN_UPGRADE:

SET SERVEROUTPUT ON EXEC DBMS_DST.BEGIN_UPGRADE(42);

执行完这条,数据库会进入PREPARE状态。此时可以查一下升级状态确认进入了流程:

SELECT PROPERTY_VALUE FROM DATABASE_PROPERTIES WHERE PROPERTY_NAME = 'DST_UPGRADE_STATE';

正常情况下PROPERTY_VALUE会变成PREPARE。然后关闭并重启实例:

SHUTDOWN IMMEDIATE; STARTUP;

重启的目的是让数据库在 PREPARE 状态下重新加载数据字典,为后续的数据改写做好准备。重启完成后,执行真正改写 TSTZ 数据的命令:

EXEC DBMS_DST.UPGRADE_DATABASE(42);

这一步执行时间取决于有多少 TSTZ 和 LTZ 数据。小库可能几分钟,大库可能跑一两个小时。执行期间数据库保持 open,但业务最好已经断开,避免中途 DDL 或者 DML 干扰升级。

UPGRADE_DATABASE执行完成后,还需要再重启一次实例:

SHUTDOWN IMMEDIATE; STARTUP;

第二次重启是为了让升级后的时区元数据在实例中全局生效。最后执行收尾命令:

EXEC DBMS_DST.END_UPGRADE;

执行完再查一次状态,确认回到NONE。这里要强调,END_UPGRADE不能漏,漏了数据库会一直处于 PREPARE 状态,后续一些操作可能会被限制。

我在参数里习惯把 42 显式写出来,而不是省略参数让它自动判断。不同环境的默认行为可能会有差异,显式写版本号能让脚本在不同库上行为一致。升级完成后,V$TIMEZONE_FILE.VERSION就会变成 42。

3.3 升级后的验证:时区版本与 TSTZ 数据

升级完第一件事不是马上导数据,而是先确认版本号和数据库对象状态。

SELECT FILENAME, VERSION FROM V$TIMEZONE_FILE;

如果看到VERSION为 42,说明数据库层面已经切换完成。接着检查失效对象:

SELECT COUNT(*) FROM DBA_OBJECTS WHERE STATUS = 'INVALID';

时区升级会失效一部分依赖时区函数的 PL/SQL 对象,这是正常现象。如果失效对象里有业务包,通常是编译一次就行:

EXEC UTL_RECOMP.RECOMP_PARALLEL(4);

RECOMP_PARALLEL的并行度按 CPU 核数调整,不用给太高,4 到 8 足够。重编译之后看一眼 TSTZ 列的实际读取是否正常:

ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM TZR'; SELECT biz_time FROM asset.t_order WHERE ROWNUM <= 5;

如果这组查询正常返回,说明数据库内部已经可以按 42 版规则解释数据。顺便说一句,历史 TSTZ 数据中的区域名可能因为规则变化在展示上跟之前略有不同,比如某些区域在新版规则里被映射到了同义区域名,这是规则升级的预期效果,不代表数据损坏。

4. 升完时区再导数据泵:导出导入参数与验证动作

4.1 重新导出时用参数文件管理参数

目标库升级完成后,数据泵的工作就回归常规了。如果源库是之前导出过 dump 的那套环境,我建议不要再用旧 dump,而是重新导一份。原因很简单:旧 dump 头部的时区版本就是当时的版本,虽然目标库升级到 42 后大概率能兼容旧 dump,但如果源库在两次导出之间还发生过其他变更,旧文件的元数据可能已经和现状不一致。

导出时我喜欢用参数文件,比命令行好维护,也方便留痕。下面是一个典型示例:

DIRECTORY=dmp_dir SCHEMAS=asset DUMPFILE=asset_%U.dmp LOGFILE=asset_exp_20250101.log PARALLEL=4

然后执行:

expdp system/xxx PARFILE=exp_asset.par

DUMPFILE里的%U配合PARALLEL=4会生成多个分片文件,比单文件在大表场景下导出更快,后面导入时也可以并行读。SCHEMAS按实际业务 schema 调整。如果只想导某几张表,可以换成TABLES=asset.t_order,asset.t_user。

这里有个细节需要注意:这个阶段源库的时区版本已经是 42,导出的 dump 头里也会写入 42。如果目标库没有先升级,还是 32,导入一定会报ORA-39405。所以顺序必须是“先升级目标库,再导数据”,不要反过来。

4.2 先用 METADATA_ONLY 试导入

正式导入大表数据之前,我会先用CONTENT=METADATA_ONLY做一次预导入。它只建表、建索引、建约束,不加载数据,耗时很短,又能提前暴露时区版本、对象冲突、权限之类的问题。

impdp system/xxx DIRECTORY=dmp_dir \ DUMPFILE=asset_%U.dmp \ SCHEMAS=asset \ CONTENT=METADATA_ONLY \ TABLE_EXISTS_ACTION=SKIP \ LOGFILE=asset_imp_meta.log

TABLE_EXISTS_ACTION=SKIP是防止目标库已经存在同名表时预导入报错。如果目标库的 schema 是全新空库,可以不加这个参数,或者改成REPLACE。

这一步如果顺利跑完,日志里不会出现ORA-39405,说明数据泵已经接受了目标库的时区版本。之后做一次元数据对比,检查预导入建的表和原库的表数量、字段类型是否一致,尤其是 TSTZ 字段。如果预导入都报时区错误,那不用怀疑,肯定还有环境没升级到位,此时去查V$TIMEZONE_FILE往往会发现版本仍然是 32。

4.3 正式导入并核对 TSTZ 字段

预导入通过后,正式导入数据。我常用的参数组合是:

impdp system/xxx DIRECTORY=dmp_dir \ DUMPFILE=asset_%U.dmp \ SCHEMAS=asset \ TABLE_EXISTS_ACTION=TRUNCATE \ PARALLEL=4 \ LOGFILE=asset_imp_20250101.log

用TRUNCATE是因为预导入阶段已经建好了表,正式导入如果还用默认的SKIP,数据不会进去;用APPEND又可能出现重复行。TRUNCATE会先清空预导入建的表结构中的数据,再重新加载,得到的是一个干净的结果。如果目标库的 schema 是全新创建的,也可以直接删掉预导入的表,然后用TABLE_EXISTS_ACTION=CREATE或者默认行为一把梭。

导入完成后,不要只看日志里的 “Successfully Loaded” 就认为万事大吉,要实际抽查 TSTZ 字段。我通常会做两步核对。

第一步,看数量:

SELECT COUNT(*) FROM asset.t_order; SELECT COUNT(*) FROM asset.t_order WHERE biz_time IS NOT NULL;

第二步,看时间内容:

SELECT TO_CHAR(biz_time, 'YYYY-MM-DD HH24:MI:SS TZH:TZM TZR') AS biz_time FROM asset.t_order WHERE ROWNUM <= 10;

如果这些查询返回的数据跟源库一致,说明 TSTZ 数据在 42 版规则下正确落库。要特别注意,如果目标库的DBTIMEZONE跟源库不同,TIMESTAMP WITH LOCAL TIME ZONE列插入后的本地时间会按目标库时区重新换算,这是正常的,但业务侧可能会觉得“时间变了”,建议提前沟通。

5. 时区升级与数据泵导入排坑指南

5.1 只打补丁不跑 DBMS_DST,版本纹丝不动

现象:数据库软件已经升级到很新的补丁级别,DBMS_DST.GET_LATEST_TIMEZONE_VERSION也返回了 42,但V$TIMEZONE_FILE.VERSION还是 32,导入 dump 照样报ORA-39405。

原因:软件补丁更新的是 Oracle Home 里的时区数据文件,数据库实例本身仍然沿用旧版本。时区文件的升级和数据库时区版本的升级是两回事,后者必须显式执行DBMS_DST流程。

解决:不要看补丁版本就以为时区版本跟着升了。执行第三章里的完整流程,确认V$TIMEZONE_FILE.VERSION变成 42 再考虑导数据。

5.2 升级中断后重复 BEGIN,状态卡在 PREPARE

现象:BEGIN_UPGRADE执行后实例重启失败,或者UPGRADE_DATABASE中途报错,此时不检查状态又直接执行了一次BEGIN_UPGRADE,结果数据库的升级状态一直停留在 PREPARE,后续操作越来越乱。

原因:BEGIN_UPGRADE是升级流程的起点,不是幂等操作。升级已经处于进行中状态时,重复开启会让DBMS_DST认为流程被交叉触发。

解决:先查DST_UPGRADE_STATE。如果状态是PREPARE或UPGRADE,说明有未完成的流程。此时优先确认日志中失败的具体环节,是实例没起来,还是数据改写没跑完。常见做法是直接继续执行UPGRADE_DATABASE(42),而不是重新走BEGIN_UPGRADE。只有确认要放弃本次升级回到旧版本时,才考虑回滚方案,而且回滚前一定要有备份。

5.3 导入没问题,应用连上后报 timezone region not found

现象:数据泵导入完全正常,数据库里也能查到数据,但应用服务器启动后,执行含 TSTZ 字段的 SQL 时报ORA-01882,提示找不到时区区域。

原因:数据库时区版本升到了 42,但应用的 JDBC 驱动或客户端自身的时区数据文件还在低版本,它不认识新版里出现或重命名的时区区域名。

解决:更新应用侧的 JDBC 驱动到与 19c 配套的版本;如果应用服务器使用 JVM,确认 Java 版本和时区数据。临时缓解可以设置-Duser.timezone=Asia/Shanghai,但这不是根治办法。这里也要理解,数据库升级不代表客户端自动升级,两端版本需要一起维护。

5.4 UPGRADE_DATABASE 期间归档日志暴涨

现象:升级窗口刚开始半小时,归档目录空间告警,日志切换频率明显高于平时,UPGRADE_DATABASE还没跑完就有撑爆磁盘的风险。

原因:时区升级会重写大量 TSTZ 和 LTZ 数据,每次数据块改写都会产生 redo,再转成归档。涉及的表越大,归档量越大。

解决:提前按 2.1 里的统计 SQL 评估数据量,给归档目录预留足够空间。升级期间还要关闭不必要的备份任务,避免备份和升级抢磁盘。实在空间紧张,考虑把升级拆成多个阶段执行,或者在测试库验证后再处理生产,而不是硬着头皮跑。

5.5 想靠 expdp VERSION 参数绕过 ORA-39405

现象:有人发现加了VERSION=12.1或VERSION=19.3之后,导出命令能正常执行,但导入端还是报ORA-39405,于是怀疑参数没生效。

原因:VERSION参数限制的是 dump 文件的数据库版本兼容性,它不会改变 dump 头里的时区版本标记。数据泵检查时区版本时,看的是源库V$TIMEZONE_FILE.VERSION,这个值不受导出参数控制。

解决:不要在这个方向上浪费时间。要么把目标库时区版本升到不低于 dump 的版本,要么在导出前做数据转换,把 TSTZ 字段先转成字符型再导出,但后者会丢失时区语义,一般只在目标库确实不能升级时才用。

6. 从验证到复用:一个时区版本检查小脚本

经历过一次这种升级后,我养成了一个习惯:在每个维护窗口前先跑一遍时区版本检查脚本,而不是等数据泵报错再去查。下面这个脚本可以直接存成check_tz.sql:

SET SERVEROUTPUT ON DECLARE v_cur NUMBER; v_latest NUMBER; BEGIN SELECT VERSION INTO v_cur FROM V$TIMEZONE_FILE; v_latest := DBMS_DST.GET_LATEST_TIMEZONE_VERSION; DBMS_OUTPUT.PUT_LINE('当前时区版本: ' || v_cur); DBMS_OUTPUT.PUT_LINE('可用最高版本: ' || v_latest); IF v_cur < v_latest THEN DBMS_OUTPUT.PUT_LINE('结论: 需要升级时区版本'); ELSE DBMS_OUTPUT.PUT_LINE('结论: 时区版本已满足导入要求'); END IF; END; /

脚本里有两点需要注意。第一,SELECT VERSION INTO v_cur依赖V$TIMEZONE_FILE只有一行返回,正常情况确实只有一行,但如果环境异常出现多行,可以改成先SELECT MAX(VERSION)。第二,脚本只做判断,不执行升级,所以可以安全地在数据泵导入前随时运行。

我通常还会把这个脚本和升级脚本放在同一个目录里,每次执行前先跑一次,看到版本满足要求再动手导数据。升级脚本里也只放命令,不放排错逻辑,保持简单。这样一套下来,再遇到ORA-39405时,我只需要查两件事:目标库时区版本够不够,dump 是不是从高版本环境导出的。这两件事确认完,问题基本就定位了。另外提醒一句,时区版本升级不是改DBTIMEZONE,如果你真正想改的是数据库默认时区,那是另一套操作流程,不要混淆。作为一线操作者,我吃过没提前检查时区文件就盲目跑升级流程的亏,那次白白浪费了半个维护窗口。希望这篇能帮你少走一次弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询