“数据库迁移”这四个字,在项目排期表上往往只占一行,真干起来却能让整个团队脱一层皮。尤其是往Oracle迁:数据量大、对象关系复杂、字符集敏感、停机窗口短,任何一个环节没盯住,轻则迁移超时,重则数据对不上账、业务起不来。我做一线DBA这些年,经手过不少Oracle迁移项目,从11g升级到19c、从MySQL异构迁到Oracle也处理过,最大的体会是:真正的难点从来不在expdp/impdp那几条命令上,而在迁移前怎么评估、迁移中怎么控风险、迁移后怎么验收。这篇就把整个链路掰开揉碎讲清楚,从方案选型到实操参数,再到常见坑的排查思路,适合准备做Oracle迁移的运维、DBA和开发同学直接当参考手册用。
1. 先把这个迁移项目的全貌看清楚
1.1 动手前先回答三个问题:迁什么、多大、能停多久
很多人接到任务,第一反应是“找个迁移工具开跑”,这个顺序是反的。Oracle迁移看起来都是导入导出,但因为场景不同,技术选型和风险等级会差出十万八千里。
先分清楚你的迁移属于哪一类。第一类是同构升级,典型的就是11g迁到19c,源和目标都是Oracle,只是版本变了、架构变了。这类迁移相对单纯,但要注意11g是传统非CDB架构,19c默认是CDB/PDB架构,直接做逻辑导入会遇到表空间、用户、权限一系列适配问题。第二类是异构迁移,比如从MySQL、SQL Server迁到Oracle,这类最麻烦的是数据类型映射:MySQL的TINYINT对应Oracle的NUMBER还是SMALLINT,SQL Server的DATETIME2精度怎么落到Oracle的TIMESTAMP,每个类型都得单独验证。第三类是跨平台迁移,比如Windows上的Oracle要迁到Linux,或者物理机迁到虚拟化平台,这类基本脱离逻辑导出导入,考虑的是数据文件平台字节序,RMAN跨平台转换才是正解。
第二个问题是数据量级。这个直接决定工具的可行性。我做过一个库,光核心流水表就有800GB,用Data Pump导出,四个并行跑了两小时,导入又花了四个小时——这个量级还可以接受。但如果你面对的是几个TB的仓库,逻辑导出导入的时间成本和临时空间成本就会非常吓人,这时候更像用Data Guard切换或者可传输表空间来做物理级迁移。
第三个问题,也是最要命的:业务允许停多久。凌晨一点到早上六点,只有五个小时窗口,和允许停一整宿,方案完全不一样。窗口短,就必须考虑准实时同步方案;窗口长,逻辑导出够用。我个人习惯是先把这三个问题写进项目文档,明确再动手。
1.2 工具矩阵:Data Pump、Data Guard、GoldenGate到底怎么选
先看一个简单的对比表,这是我在选型时常用的参照。
| 方案 | 适用场景 | 停机时间 | 复杂度 | 成本 |
|---|---|---|---|---|
| Data Pump逻辑导出导入 | 中小库、同构升级、异构小规模迁移 | 数小时以上 | 低,参数透明易排错 | 无额外License |
| 可传输表空间 | 大数据量、同版本同平台迁移 | 分钟级 | 中,依赖文件复制和元数据导入 | 无额外License |
| Data Guard切换 | 同构同平台、大库、跨版本升级 | 分钟级 | 中,需要网络与归档条件好 | 需Enterprise Edition |
| GoldenGate | 异构迁移、停机几乎为零 | 秒级 | 高,进程多、配置复杂 | License成本高 |
| SQL Developer迁移工作台 | 小型异构迁移 | 可控 | 低,可视化操作 | 免费 |
这里多说一句Data Guard。很多人不知道,Oracle迁移里Data Guard不只是灾难恢复用的。只要源库和目标库是同平台、同架构,完全可以用Data Guard把目标库搭起来,日志应用追平后做一个switchover,数据库状态直接切换,数据零丢失、停机只有几分钟。我负责过一个某公司交易系统迁移,源库7TB,就是靠这招在一个周末把整个库从旧机房搬到了新环境,源端几乎没停过机。但它有前置条件:两端版本可以跨一个主版本,平台字节序要一致,网络带宽要能扛住归档日志的实时传输。
GoldenGate同同样能实现准实时迁移,而且支持异构,但复杂度摆在那里:抽取进程、传输进程、复制进程,每个都要单独配置和管理,初始化加载还有一堆校验要做。如果没有严格的RTO要求,我不建议动不动就上GoldenGate,成本高不说,运维门槛也高,出了问题排错比Data Pump难多了。
说到底,工具选型本质是“停机窗口、数据量、预算”三者的妥协。我的建议很朴素:能用逻辑导出解决的,就不要堆砌复杂方案;复杂度每增加一个层级,迁移失败的概率也会肉眼可见地上升。
2. 迁移前的摸底:这几项检查能让你少加一周班
2.1 字符集:看似无关紧要,出事就是事故
做Oracle迁移的人,如果真的只记住一件事,那应该是:先查字符集。字符集一旦不匹配,导进去的数据可能不是乱码,而是“看起来正常其实已经错了”。举例来说,源库是ZHS16GBK,里面存了一个生僻字,迁到AL32UTF8,正常情况下没问题,因为UTF8可以容纳更多字符。反过来,AL32UTF8的库里存了四字节扩展字符,要迁到ZHS16GBK,对不起,直接报错或者被替换成问号。更阴险的是,源库虽然是GBK,但某些历史数据里混入了非法编码,导出的dmp文件到了目标端,那一列数据就变成了一串奇怪的“¿¿”。
我在实操中查字符集,第一屏是用这几条SQL:
SELECT userenv('language') FROM dual; SELECT parameter, value FROM nls_database_parameters WHERE parameter LIKE '%CHARACTERSET%'; SELECT parameter, value FROM nls_session_parameters WHERE parameter LIKE '%CHARACTERSET%';第一条是会话字符集,第二条是数据库字符集,这俩一定要弄清,因为客户端NLS_LANG设错了也会造成“假乱码”:数据在库里面是好的,只是SQLPLUS或工具显示乱。迁移前至少要在源库和目标库执行一次全套字符集确认,并记录下来。
经验之谈:迁移前抽一张小表,插几条包含中英文、数字、特殊符号(比如©、®、€以及GBK扩展区的字符)的探针数据,跟着整个迁移流程走一遍,到目标库查出来比对。这一步看着土,但能省掉后续一大堆返工。字符集问题一旦在数据导完后才发现,基本等于拔数据重来。
2.2 对象盘点和兼容性检查:搬家前先列遗物清单
迁移数据库,技术上像搬家:搬之前你得知道家里到底有多少东西、哪个柜子最重、哪些东西有依赖关系。我在每个迁移项目里都会强制做一次对象盘点,并且要求输出一份可签核的清单。
至少包含这么几类:表和分区表数量、单表数据量TOP10、LOB字段的表列表、索引数量与类型、约束(主键、外键、唯一、Check)、序列、同义词、物化视图、DBLINK、Job、存储过程/函数/包/触发器。不容易一眼看到但特别重要的,是那些隐藏的依赖——比如一张A表的外键引用B表,B表又被物化视图引用,导入顺序不对就会连环报错。
我常用的盘点SQL类似这样:
-- 每个用户下的对象概览 SELECT owner, object_type, COUNT(*) FROM dba_objects WHERE owner IN ('APP_USER') GROUP BY owner, object_type ORDER BY 1,2; -- 单表行数TOP10(非精确但够用) SELECT owner, table_name, num_rows FROM dba_tables WHERE owner='APP_USER' ORDER BY num_rows DESC FETCH FIRST 10 ROWS ONLY; -- 无效对象列表 SELECT owner, object_name, object_type, status FROM dba_objects WHERE owner IN ('APP_USER') AND status != 'VALID';注意,dba_tables里的num_rows只是统计信息里的行数,可能滞后,但用来排序判断哪些是大表是够用的。
兼容性检查同样不能跳过。从11g迁到19c,很多老语法和特性在19c里可能有变化。比如11g里常用的CONNECT角色在19c中默认权限已经被收敛,迁移后应用账号可能连不上;11g的exp/imp经典模式在19c中官方已经不推荐支持,虽然还能跑,但别拿生产开玩笑。还有分区表的语法,11g的RANGE分区配上子分区策略,在19c的Partitioning增强里未必还能用同一套管理脚本。这些不是导入导出工具能替你发现的,必须在迁移前通过源库的对象清单逐项核对。
2.3 停机窗口与回滚预案:迁移失败的底线思维
很多团队做迁移只盯着“怎么导过去”,却没人想“如果导不过去,怎么退回来”。一个没有回滚预案的迁移计划,等于没穿安全绳就上高空。我在项目里坚持两条:一是有明确回滚触发条件,二是有可执行的回滚步骤。
回滚方案的底气来自源端数据保留。最稳妥的做法是迁移期间源库保持只读或者至少保留完整备份,迁移完成后也不要立即销毁源环境,通常保留一到两周观察期。应用层那边,连接串要提前规划好,切换后如果出现问题,能在几分钟内把流量切回旧库。连接池、DNS切换、JNDI配置这些听起来简单,但实际演练过的人都知道,光是“怎么把正在跑的Java应用切到新库再切回来”这一件事,就够整个运维组喝一壶。
停机窗口的确认要实际演练验证。纯粹拍脑袋说“晚上业务量低,两小时够了”不靠谱。正确做法是:提前一周做一次完整演练,记录导出耗时、传输耗时、导入耗时、校验耗时,把每个节点的时间都记下来,然后乘以1.5到2倍作为真实窗口预算。我在一个4.2TB的项目里,演练时Data Pump导入第一轮跑了5.5小时,比预估多了整整两小时,就是因为没算上目标库服务器磁盘IO比预想低这个变量。没有演练教训,正式切换那晚就是救火现场。
3. 实操:Data Pump迁移从导出到校验的完整链路
3.1 导出侧:expdp关键参数与一致性保障
选定了Data Pump方案后,第一步是在源库准备目录对象并给数据库账号授权:
CREATE DIRECTORY dpdir AS '/u01/dpdata'; GRANT READ, WRITE ON DIRECTORY dpdir TO system;目录要提前确认空间,尤其注意不要把dmp文件放在和源库数据文件同一块磁盘上,否则导出过程中空间被吃满,库就直接出报警了。关于空间估算,我之前说过一个简单经验:先查dba_segments里目标schema的总大小,导出后的dmp一般会比逻辑大小小,因为不包含未使用的段空间,但仍然建议预留源端segment总大小的1.2倍以上空间,多留没坏处。
接下来导出命令我一般是这么写的:
expdp system/***@prod DIRECTORY=dpdir \ DUMPFILE=app_user_%U.dmp LOGFILE=export_app_user.log \ SCHEMAS=app_user PARALLEL=4 COMPRESSION=ALL \ FLASHBACK_TIME=SYSTIMESTAMP几个关键参数挨个说。SCHEMAS指定业务用户,比导全库可控,而且方便后续按schema做映射。DUMPFILE里的%U配合PARALLEL,是并行导出的分片文件命名方式,比如8个并行就生成8个文件,导入时用通配符就能一把收进来。COMPRESSION=ALL是压缩dmp文件内容,能显著减少传输量,但会增加源端CPU开销——如果源库CPU本来就忙,建议只设COMPRESSION=DATA或干脆不压。
FLASHBACK_TIME=SYSTIMESTAMP这个参数容易被忽略,但很重要。它的作用是让导出会话基于闪回查询去读一致性的数据快照,而不是简单地从当前时刻开始逐表扫。没有这个参数,大表小表导出的时间点可能不同,表A导了一半业务还在写,表B导出时表A又被更新了,最后导出来的数据在业务逻辑上是“拧巴”的。加了FLASHBACK_TIME,这个值一定会占用UNDO表空间,所以导出前要确认UNDO足够大,否则会报ORA-01555快照过旧。
并行度是个精细活。不要只看CPU核数就把PARALLEL开到最大。一次在一个16核的库上我开到16并行,结果磁盘在IO层直接堵死了,导出速度反而下降。正确的做法是先看源库的IO能力,一般的规律是:如果底层是SSD,可以往核数靠;如果是机械盘,2到4个并行足够,再多就是内耗。启动导出后可以通过下面的SQL实时看会话进度:
SELECT username, opname, target_desc, sofar, totalwork, ROUND(sofar/totalwork*100,2) AS pct_done FROM v$session_longops WHERE opname LIKE '%APP_USER%' AND totalwork != 0;3.2 导入侧:impdp映射配置与提速技巧
导入到目标库,命令看起来对称,但参数更考验经验:
impdp system/***@target DIRECTORY=dpdir \ DUMPFILE=app_user_%U.dmp LOGFILE=import_app_user.log \ SCHEMAS=app_user \ REMAP_SCHEMA=app_user:app_user \ REMAP_TABLESPACE=old_ts:new_ts \ TRANSFORM=SEGMENT_ATTRIBUTES=N:TABLE:old_ts \ EXCLUDE=STATISTICS PARALLEL=4 TABLE_EXISTS_ACTION=SKIPREMAP_SCHEMA用来把源用户映射到目标用户,两边用户名一致也可以不写,但为了明确起见写上没坏处。REMAP_TABLESPACE解决的是表空间不匹配的问题:11g里业务数据在USERS或自定义表空间,19c导入到PDB里很可能是另一个名字,不做重映射就直接失败或建到默认表空间里。TRANSFORM=SEGMENT_ATTRIBUTES=N:TABLE是让我又爱又恨的参数,它会让导入跳过源端的存储属性(比如INITIAL、NEXT等段尺寸),用目标库默认的表空间属性来建表,好处是避免把源端已膨胀的段尺寸设置搬过来,坏处是如果目标库默认表空间配置不合理,导入后会产生大量小碎片。我一般建议用,同时把目标库的默认表空间属性提前调好。
EXCLUDE=STATISTICS这个参数建议一直带上。源端老版本的统计信息到了新版本,大概率参考价值不大,比如11g的统计信息和19c优化器需要的直方图细节未必兼容。导入后在目标库重新收集一遍统计信息,效果远比沿用旧的稳。TABLE_EXISTS_ACTION=SKIP是为了支持分段导入:第一轮先导元数据,第二轮导数据,第三轮再补索引约束,中途失败也不用清库重来。
如果数据量很大,建议把导入拆成两段。先只导结构和元数据:
impdp ... CONTENT=METADATA_ONLY EXCLUDE=STATISTICS再并行导入数据:
impdp ... CONTENT=DATA_ONLY EXCLUDE=STATISTICS TABLE_EXISTS_ACTION=TRUNCATE最后再单独创建索引、约束和补统计信息。这样做的原因是,DATA PUMP在处理整库导入时会把所有对象塞进一个巨大的依赖图里解析,表越多越慢。拆开之后,元数据阶段通常几分钟完成,数据导入阶段能放开手脚并行,索引约束最后批量建反而比导入时逐张表建快很多。注意,拆分后要自己在导入完成后确认外键有没有补全,因为DATA PUMP默认处理依赖,但分段的场景下它不会帮你维护全过程的依赖关系。
3.3 剪裁前的验收:行数、对象、编译、拨测一个都不能少
导入完成不等于迁移完成,只等于“数据文件进去了”。在我这里,验收清单不过完,不允许动连接切换。
第一步是行数比对。不要指望完全一致地逐表count,大表统计太费时间。我常用的方式是按schema下所有业务表做一次快速对比:先对比dba_tab_modifications或者迁移时记录的快照行数,再对核心流水表抽表精确count。如果有对不上的,立刻查看导入日志里是否有表报错,而不是盲目重导。
第二步是对象状态检查。导入后第一件事就是查无效对象:
SELECT owner, object_type, object_name FROM dba_objects WHERE status != 'VALID' AND owner='APP_USER' ORDER BY object_type, object_name;查到无效对象后,用Oracle自带的工具批量重编译,比如调用系统包:
BEGIN DBMS_UTILITY.COMPILE_SCHEMA(schema => 'APP_USER'); END; /或者用UTL_RECOMP并行编译。重编译后再次查询,直到无效对象清零为止。经常有人忽略这一步,觉得只要表在就行,结果业务一跑,某个存储过程直接报“无效的对象”,那时候再去查代码,纯粹是给自己找活干。
第三步是约束状态和序列对齐。序列是个特别隐蔽的坑:系统迁移前主键ID已经到了98000,但序列还在从1开始或者从一个很小的值开始,第二天业务一跑就主键冲突。迁移前记下所有源端序列的last_number,迁移后在目标库按需调高。比较实用的一条SQL是从dba_sequences里批量取last_number,然后对目标序列做alter sequence,或者直接用工具调整到“当前表最大ID+1”。
最后是业务拨测。这一环节必须有业务方和开发配合,场景越贴近真实越好:登录、下单、改密、报表查询、定时Job、文件导出,各跑一遍。拨测不只是“能通就行”,还要核对数据内容,尤其是金额、日期、状态字段,拿一条源库业务记录去目标库回查,确认完全一致。
4. 常见问题与排查技巧实录
4.1 乱码的三种来源与定位思路
乱码是Oracle迁移里出现频率最高的“灵异事件”。我在处理这类问题时,习惯先把乱码分成三种来源:
第一种是数据库字符集本身不匹配,导致dmp文件里的编码在目标库解释不了。这种最严重,一步错步步错,数据已经落盘错了,光改显示层是救不回来的。第二种是客户端NLS_LANG设置不对。库和dmp都是好的,只是SQLPLUS或应用连接串上没有指定字符集会话,结果客户端用了US7ASCII之类的编码去解释UTF8数据,显示出来是乱码。这种只需把NLS_LANG设成和数据库字符集一致就能解决,数据本身无损。第三种是应用代码问题。最常见的是JDBC连接串里没有characterEncoding,Java程序的中文在页面乱码,这种和数据库迁移其实没关系,但每次迁移完都会被揪出来,因为平时大家不太注意。
排查顺序我从不错乱。第一步先查目标库数据:用SQLPLUS连上去,直接select那几条已知的探针数据,如果SQLPLUS里都是正确的,那数据库层面没问题;如果SQLPLUS里就乱,那直接查数据库字符集和dmp来源。如果SQLPLUS良好但应用显示乱码,那就去查应用连接字符集和JDBC配置。这样一层层切,基本能在十分钟内定位。
4.2 约束、外键与序列:迁移界的经典三连坑
导入时最常报的错就是外键相关:ORA-02291(违反完整约束条件,子表找不到父键)、ORA-02292(违反完整约束条件,父键被引用)。出现场景基本是分段导入时表数据到位顺序不同,外键在数据没齐前就校验了。处理办法:导入阶段临时禁用外键,数据全部就位后重新启用。启用时可以用NOVALIDATE来加快速度,因为数据已经导过一遍,逻辑上是对的,校验全量反而是浪费:
ALTER TABLE child_table ENABLE CONSTRAINT fk_name NOVALIDATE;批量生成这种SQL也不难,dba_constraints里查出所有外键约束,拼出来直接执行。注意,NOVALIDATE只跳过存量数据校验,新写入的数据依旧会被约束检查,业务安全性不受影响。
序列的三连坑比外键还日常。迁移后第一天上线,凌晨就收到主键冲突报警,不敢说100%是序列问题,但大概率就是。除了前面提到的“序列没对齐”,还有一个细节是Oracle的序列默认CACHE值。如果迁移后应用并发量变大,序列缓存值太小,频繁访问序列会增加字典锁竞争,表现为应用获取ID变慢。跨版本迁移后数据库行为有变化,这个问题容易被忽略。
4.3 性能断崖式回退:先查环境,再查SQL
迁移后性能明显变差,是我收到最多的投诉之一。但这里有一个我的原则:先别急着改SQL,先检查环境。
第一件事是统计信息。我说过导入时习惯EXCLUDE=STATISTICS,但如果导入后忘了重新收集统计信息,优化器就是在“盲猜”,执行计划差一个数量级完全是正常的。收集全schema统计信息用一条命令就行:
BEGIN DBMS_STATS.GATHER_SCHEMA_STATS(ownname => 'APP_USER', cascade => TRUE, degree => 8); END; /第二件事是检查执行计划变化。11g迁到19c,优化器很多行为都变了,比如19c默认启用自适应计划,有些在11g走索引的SQL到了19c可能因为执行成本被重新计算而走向全表扫描。这时候可以从v$SQL里拿SQL_ID,对比两个版本的执行计划,如果确定是优化器行为变化,可以用SQL Plan Baseline把旧计划固定下来。
第三件事是高水位和行迁移。如果源库用了很久,表高水位很高,导到目标库后DATA PUMP重新建表,高水位会被重置,这个通常是好事。反而是如果直接用可传输表空间方式迁移,高水位和行迁移都会原样带过去,这类问题就得靠收缩表和重建索引解决。
还有一个经常被冤枉的因素是网络。应用连的数据库变了,但应用服务器的网络路由表没变,走了新的链路,延迟变大。别问为什么我知道,问就是踩过:迁移后业务说“数据库好慢”,结果用tkprof和AWR查了半天,最后发现是应用到新库的网络绕了两跳。
最后分享一个自己的土办法
每次做迁移,我都会准备一本“迁移笔记”:源端字符集、版本、对象数量、大表清单、序列基线、导入耗时、每条命令的输出摘要、所有报错和处理方式,全部记下来。这次没用到的东西,下一次可能就是救命信息。还有一个习惯了多年的小技巧:正式开跑前,先在目标库建一个同结构的样板schema,导入一两张小表,验证字符集、权限和映射参数,全部确认无误后再动全量。这个准备工作只要十分钟,但每次都能帮我提前暴露一两处配置问题,把真正的大规模迁移变成一次“重复劳动”。迁移这事,胆子要大,心要细,前戏做足,后面就是走流程。