搞Oracle到PostgreSQL迁移这件事,圈子里聊得最多的不是工具选哪个,而是"怎么让业务别停太久"、"数据到底搬对没有"、"出了问题能不能跑回去"这三个灵魂拷问。我最近完整跟完了一个核心交易系统的迁移项目,正好把整套打法梳理一遍,给准备上车的朋友当个参考。
先说结论:Oracle迁PostgreSQL,难点从来不在"怎么导数据",而在迁移链条的整体设计。你只有把"低业务中断、可校验、可回退"当成一个整体目标来拆解,才能避免那种"数据导完了、业务起不来、回也回不去"的尴尬局面。
1. 迁移项目启动前:先想清楚"怎么切"再动手
1.1 迁移背景与目标拆解
我们这个项目是从Oracle 11g迁到PostgreSQL 14,源库大概有3TB数据,核心业务表最大的一张超过8亿行,还有不少用到了CLOB、分区、存储过程的老业务。业务侧给的要求很明确:停机窗口最多4小时,切换后一个月内如果发现重大问题必须能回退,并且迁移完成后要拿出让业务信服的数据一致性证明。
这三条要求翻译成技术语言就是:
- 低业务中断:不能搞"先停库全量导出再全量导入"的老套路,必须走全量+增量同步的双轨方案,把真正的停机时间压缩到只需要处理增量追赶和切换验证这两个环节。
- 可校验:迁移完成后,要用行数、校验和、抽样比对等手段证明数据没丢、没多、没错。不是拿两条SQL对一下数量就完事,而是要能经得起业务和审计的追问。
- 可回退:切换后保留完整的回退通道,包括源库持续运行、反向同步机制、回退演练预案。一旦业务验证不通过,能在一个小时内切回Oracle,且不丢新增数据。
这三个目标不是并列关系,而是互相牵制的。比如为了可回退,你可能需要在一段时间内同时维护两套库的写入;为了低中断,你的增量同步工具必须足够稳定,否则追赶不上就会无限拉长停机窗口;为了可校验,又必须在迁移过程中保留足够的审计痕迹,方便回退时对账。
1.2 迁移前的源库调研与风险清单
很多项目死在第一步:没搞清楚源库里到底有什么就开始动手。我建议迁移前至少花一到两周做一次彻底的源库体检,输出一份风险清单。我们当时重点排查了以下几类东西:
- 对象清单:表、索引、约束、序列、视图、物化视图、存储过程、函数、包、触发器、同义词、DB Link。每一项都要统计数量并评估改造工作量。千万别小看同义词和DB Link,业务SQL里如果大量使用,迁移后全都要改。
- SQL使用情况:抓取AWR报告和近一段时间的慢SQL,重点看用了哪些Oracle独有语法,比如
CONNECT BY层次查询、(+)外连接、ROWNUM分页、NVL、SYSDATE、MERGE、LISTAGG等。这些是SQL改造的重灾区。 - 数据类型分布:NUMBER、VARCHAR2、DATE、CLOB、BLOB、LONG、RAW、ROWID等类型的字段数量和最大长度。尤其是CLOB和LONG,迁移后要用TEXT替代,但业务代码里对CLOB的操作方式可能需要调整。
- 存储过程复杂度:统计有多少存储过程、总代码行数、用到了哪些Oracle独有包(DBMS_SQL、DBMS_JOB、DBMS_ALERT、UTL_FILE等)。这部分改造成本往往被严重低估。我们项目里有1300多个存储过程,最终花了整个项目40%的工时在改写上面。
- 依赖关系:应用与数据库的交互方式,包括连接方式(JDBC、OCI、ODBC)、事务隔离级别、会话级参数设置、临时表使用情况。应用层的连接串、驱动、SQL写法都要纳入改造范围。
体检报告出来后,要让业务方、DBA、开发三方一起评审,明确哪些东西可以"原样迁",哪些必须"改造迁",哪些是"僵尸对象"可以果断抛弃。这一步做扎实了,后面所有的方案才有依据。
2. 低业务中断的核心打法:全量+增量双轨迁移
2.1 迁移工具选型与取舍
工具选型没有标准答案,核心看你的源库版本、目标库版本、数据总量、同步实时性要求和团队熟悉度。我们当时对比了三条路线:
- Ora2Pg:开源免费,Perl写的,能把表结构、数据、序列、视图、存储过程整体转换,支持直接连Oracle导出再导入PostgreSQL。优点是自动化程度高,缺点是大表导出用COPY方式,对源库有一定压力,而且存储过程转换质量一般,只能转个骨架,复杂逻辑基本还得人工改。
- pgloader:同样开源,从Oracle读数据到PostgreSQL非常快,用的也是COPY协议,支持并行加载。但DDL迁移能力弱,主要用来搬数据。
- 自研抽取+同步脚本:用Python写一套基于JDBC的全量导出和增量抽取脚本,配合调度平台实现断点续传和数据校验。灵活性最高,但开发量不小。
我们最终采用了"Ora2Pg转结构+pgloader搬全量+自研增量同步脚本+Debezium做变更捕获"的组合方案。结构转换用Ora2Pg生成初始DDL,再人工修;全量数据用pgloader并行加载;增量部分考虑到源库是Oracle 11g,Debezium的Oracle插件要装额外组件,最后是自己写了一套基于日志的增量抽取程序。这里给个建议:如果是Oracle 12c及以上,Debezium是较好的增量同步方案,社区活跃且支持断点续传;11g就得掂量掂量了。
2.2 全量迁移的并行与限速策略
全量数据迁移最容易翻车的地方是:导太猛,把源库IO打满,业务直接告警;导太慢,又赶不上停机窗口。所以并行度和限速必须提前设计。
我们按表维度拆分任务,用了一个简单的规则:
- 小于100万行的表,单表单任务,顺序执行;
- 100万到1000万行的表,按主键范围拆成4到8个分片并行;
- 超过1000万行的表,按主键范围拆成16到32个分片,每个分片独立连接、独立事务。
同时设置了全局并发上限,保证源库的活跃会话数不超过20。pgloader本身有个batch concurrency参数可以控制并行度,但实测下来对Oracle源库支持一般,所以我更推荐自己写一个简单的调度器:用一张任务表记录每个分片的状态,调度线程从里面捞待执行的任务分发给工作线程,工作线程处理完更新状态,出错自动重试三次,还是失败就进入人工处理队列。
这里有个非常关键的细节:全量迁移必须在增量同步启动之前完成,并且要记录全量导出的数据快照时间点。我们的做法是:全量开始前先在源库开启归档日志和补充日志,然后启动增量抽取程序实时解析日志写入Kafka;全量导出的每张表在导出前记录一个SCN(System Change Number)快照,导出完成后用这个SCN到增量同步里做数据拼接。这样才能保证全量导出期间产生的增量变更不会丢。
2.3 增量同步与业务双写设计
增量同步的目标是让PostgreSQL和Oracle的数据差距保持在分钟级以内,这样停机切换时只需要把最后几分钟的增量追上就能关窗口。
我们的增量链路是:Oracle归档日志 → LogMiner解析 → Kafka → 自研消费者程序 → 应用到PostgreSQL。为什么用LogMiner而不是直接用OGG,主要是因为OGG授权成本和部署复杂度在当时的环境下不划算。LogMiner有几个坑要提前踩:
- 必须提前开启最小补充日志(Supplemental Log),否则LogMiner拿不到更新前的字段值,UPDATE语句没法正确重放。
- LogMiner解析出来的DDL语句不能直接拿到PostgreSQL执行,需要做语法转换,比如
ALTER TABLE xxx ADD (col NUMBER)要改成ALTER TABLE xxx ADD COLUMN col NUMERIC。 - 大事务解析会产生大量Redo记录,LogMiner内存可能扛不住,要设置合理的
UTL_FILE落地目录,分段读取。
增量应用端的冲突处理也需要设计。双轨运行期间,如果应用还在继续写Oracle,我们的增量程序会把这些变更实时投递到PostgreSQL,两边数据保持一致。但如果某个时刻增量应用出现问题、落后太多,或者业务在验证阶段直接在PostgreSQL上面做了修改,就会产生回环冲突。我们的策略是:切换之前,所有写操作只走Oracle,PostgreSQL只接受增量投递的历史数据;切换之后,应用层切到PostgreSQL,Oracle那边不再接收业务写流量。这套"单向写、双向读"的策略在切换前把冲突可能性降到最低。
2.4 停机窗口内的切换步骤清单
就算增量同步再稳定,切换当天也必须有SOP,每一步到什么状态、由谁确认,都要提前定义好。我们的切换清单大概是这样的:
- 业务侧发布维护通知,应用层开启只读模式或直接停止写流量;
- 等待增量同步追上,确认Oracle和PostgreSQL的数据延迟为0;
- 停止Oracle侧的写操作,记录停止时间点;
- 增量程序消费完Kafka里所有消息,应用完最后一笔变更;
- 执行数据校验脚本(下一节细说),确认两边数据一致;
- 关闭增量同步程序,防止PostgreSQL继续接收Oracle的变更;
- 应用层修改数据库连接配置,切换到PostgreSQL;
- 执行一批关键的冒烟测试SQL,确认核心业务功能正常;
- 观察5到10分钟,确认无异常后对外宣布切换完成。
整个切换过程耗时大概1.5小时,算上全量数据校验、应用重连、缓存预热,最终停机窗口控制在3小时左右,满足4小时的要求。这里要注意:第5步的数据校验不能等全部跑完才判断,我们在切换前就做过多次全量预校验(在增量持续运行的情况下),切换当天只做增量部分的差异性校验和随机抽表比对。
3. 数据校验方案:让业务信服的三个层次
3.1 第一层:行数与总量校验
最基础的校验是行数比对。对每张表执行SELECT COUNT(*),两边对不上就说明有问题。但COUNT在大表上很慢,动辄几分钟甚至更久。我们当时是用pg_comparison这类工具直接对比,但更通用的做法是:按主键范围分段统计行数,每段一个任务并行跑,最后汇总。
如果源表和目标表都没有主键(Oracle里确实存在这种烂表),那校验就只能退而求其次:按所有字段做GROUP BY后再COUNT,或者用COUNT(*)配合SUM(HASH)来粗查。这类表数量不多还好,如果很多,强烈建议在建表阶段就帮它们补上主键或唯一约束,否则后续的数据一致性验证基本没法做。
行数校验是"必要条件但不是充分条件",行数一样不代表数据一样,所以还要做第二层。
3.2 第二层:字段级CRC校验与哈希比对
第二层是对每一行的所有字段拼起来做哈希,两边比对哈希值是否一致。具体做法是:
- 在源库(Oracle)写一段SQL,把每行需要比对的字段用
DBMS_CRYPTO.HASH或STANDARD_HASH算出一个哈希值:
SELECT id, STANDARD_HASH(col1 || '|' || col2 || '|' || col3, 'SHA256') AS row_hash FROM my_table;- 在目标库(PostgreSQL)用同样的拼接规则算哈希:
SELECT id, encode(sha256(convert_to(col1 || '|' || col2 || '|' || col3, 'UTF8')), 'hex') AS row_hash FROM my_table;两边按主键关联,对比row_hash是否一致,把所有不一致的主键ID捞出来交给业务确认。
这里有几个坑必须注意:
- 拼接字符串里的分隔符不能和字段值本身冲突。我一般用两个特殊字符,比如
||'~|~'||,并提前检查字段值里是否包含这些字符。 - 字段值里的NULL要统一处理。Oracle里空字符串就是NULL,而PostgreSQL里空字符串和NULL是不同的。我们统一约定:NULL或空串都替换成一个特殊标记,比如
'<NULL>'。 - 浮点类型(BINARY_DOUBLE、NUMBER带小数)在Oracle和PostgreSQL二进制存储上有差异,直接拼接字符串做哈希可能出现"看起来一样、哈希不一样"的误报。处理方法是对数值类型先做格式化,统一保留到小数点后6位或更多,确保两边字符串一致。
- 大数据量下表字段特别多时,哈希计算会非常消耗源库CPU。我们当时在源库用只读快照或者备库上跑哈希校验,避免影响生产。
3.3 第三层:业务规则抽样比对
哈希校验能保证技术层面的数据一致,但业务方更关心"他们用起来对不对"。所以我们额外设计了一套业务规则的抽样比对:
- 从核心业务表里随机抽取若干个ID,把该ID关联的订单、明细、流水全查出来,业务方人工核对;
- 把一批统计报表SQL分别在Oracle和PostgreSQL上跑一遍,比对聚合结果是否一致;
- 把几个典型的复杂查询(多表关联、子查询、窗口函数)在新库上执行,业务方确认执行结果和口径符合预期。
这个环节的意义不止于"查数据",还在于让业务方建立对迁移结果的信任。技术团队说一百句"校验过了",不如业务方自己抽几个单子确认来得踏实。
3.4 校验工具与自动化
我们整个校验体系是跑在一个自研的校验平台上的,支持配置表清单、校验类型、阈值和通知渠道。技术上不复杂,核心就是一个任务调度器加一堆校验脚本。不过如果你没有自研条件,有几个现成工具可以参考:
- pg_comparison:专门做PostgreSQL与PostgreSQL/Oracle等多库数据对比,支持行数、全量、抽样对比,能输出差异报告。
- DataGrip/DBeaver的数据对比功能:适合小表、交互式核对,不适合大批量自动化。
- xtc校验工具:一些开源实现里有基于CRC或MD5的文件级校验能力,但直接用在数据库迁移上还是得改造。
我的建议是:工具只是辅助,校验方案和口径才是核心。先把口径和字段映射规则定义清楚,工具只是帮你把规则跑起来。
4. 可回退机制:给业务一颗定心丸
4.1 回退方案设计的两种路线
回退最理想的方案是"双活",也就是切换后Oracle和PostgreSQL都保持可写,应用层按比例把流量分摊到两边,某一侧出问题可以随时把流量全部切到另一侧。但双活对数据一致性的要求极高,写入冲突处理的复杂度也很大,普通项目很难真正做到。
我们实际采用的是"切换后保留源库只读,反向同步增量"的半双活方案。具体来说:
- 切换时,Oracle侧停止业务写入,但数据库实例不关闭,归档日志继续保留;
- 切换后,增量同步程序反向工作:监听PostgreSQL的变更(用PostgreSQL的逻辑复制功能),把变更同步回Oracle;
- 如果业务验证期间发现重大问题需要回退,我们先把PostgreSQL写入停止,等最后一批变更同步到Oracle,然后应用连接切回Oracle,就能在较短时间内恢复服务。
这个方案的关键在于反向同步的可靠性和时延。PostgreSQL的逻辑复制(Logical Replication)机制比较成熟,但把逻辑复制的变更重放到Oracle端还是得靠自研程序或中间件。我们当时是写了一个消费者,从PostgreSQL的WAL里解析出变更,再合成SQL语句应用到Oracle。这里有个现实问题:PostgreSQL逻辑复制默认只适合PostgreSQL到PostgreSQL,跨库重放得自己处理类型映射和SQL方言差异,工作量不小。
4.2 回退窗口与观察期设计
回退不是无限期的,业务方也不能"想什么时候退就什么时候退"。我们在项目计划里明确了一个回退观察期:切换后一个月内,业务如果发现重大数据或功能问题,启动回退流程;超过一个月,认为迁移已经稳定,不再支持自动回退,Oracle源库进入只读归档状态。
这个观察期不是随便定的,是根据业务体量和风险容忍度协商出来的。观察期越长,反向同步的维护成本和资源成本越高;太短,业务不放心。我见过有些项目观察期只有一周,结果第四天暴雷,回退的时候反同步数据量太大,差点没退干净。一般来说,业务量大的核心系统,观察期建议不低于两周,最好是一个月;非核心系统可以适当缩短。
4.3 回退演练:不能等到出事才练
回退方案写了几页纸,但真正出事的时候能不能按流程执行,只有练过才知道。我们在正式切换之前做了两次完整的回退演练,每次都能暴露问题。
第一次演练暴露出一个经典问题:切换后业务在PostgreSQL上生成了大量新数据,回退时要把这些数据反向同步回Oracle,结果因为表结构里有个字段类型映射不一致,导致批量INSERT失败。这类问题如果不演练,真到回退的时候就是灾难。第二次演练我们提前修正了映射,又测了流量切换的脚本,确认应用重启后能读到Oracle的最新数据,整个回退耗时控制在40分钟内。
所以我的建议是:回退演练必须包含数据层和应用层两层验证。数据层验证反向同步是否完整、一致;应用层验证连接切换、缓存清理、配置修改是否顺利。只测数据不测应用,回退后业务还是起不来。
5. 常见问题与排查技巧实录
整个迁移过程中,我们踩过的坑和排查过的问题不少,挑几个有代表性的写在这里,供大家参考。
5.1 Oracle的CONNECT BY在PostgreSQL里怎么改
Oracle的层次查询CONNECT BY PRIOR是迁移的高频改造点。PostgreSQL没有直接对应的语法,要用递归CTE(WITH RECURSIVE)改写。比如Oracle里:
SELECT emp_id, mgr_id, LEVEL FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id;改成PostgreSQL:
WITH RECURSIVE emp_tree AS ( SELECT emp_id, mgr_id, 1 AS level FROM emp WHERE mgr_id IS NULL UNION ALL SELECT e.emp_id, e.mgr_id, t.level + 1 FROM emp e JOIN emp_tree t ON e.mgr_id = t.emp_id ) SELECT emp_id, mgr_id, level FROM emp_tree;这里注意,递归CTE的性能调优空间比较大,如果树的深度和宽度都很大(比如超过10层、几十万节点),建议提前用真实数据量压测,必要时加search_depth之类的优化手段。
5.2ROWNUM和ROW_NUMBER()的等价改写
Oracle里用ROWNUM做分页很常见:
SELECT * FROM ( SELECT t.*, ROWNUM rn FROM big_table t ) WHERE rn BETWEEN 101 AND 200;PostgreSQL里直接用LIMIT/OFFSET:
SELECT * FROM big_table ORDER BY id LIMIT 100 OFFSET 100;这里有个容易踩的隐形坑:Oracle的ROWNUM是在排序之前生成的,所以如果原SQL里写了ORDER BY再套ROWNUM,语义上是"先取前N行再排序",和PostgreSQL的LIMIT语义(先排序再取前N行)不一样。改写时必须先确认原SQL的业务意图,不然数据就错了。
5.3 CLOB字段迁移到TEXT后的性能问题
Oracle的CLOB在PostgreSQL里一般对应TEXT类型。看起来简单,但实测发现一个现象:某些业务对CLOB字段做频繁的UPDATE和SELECT,在Oracle里有专门的LOB段和空间管理,性能还行;到PostgreSQL的TEXT上,如果写入的文本很大(几十KB到几MB),TOAST机制会触发压缩和外部存储,性能会有波动。
我们的处理办法:把大文本字段单独拆到一张扩展表里,和主表用外键关联,需要时才JOIN出来。如果业务确实要频繁读写大文本,还可以把external存储打开,避免每次查询都加载整个大字段。
5.4 空字符串与NULL的语义差异
Oracle里''和NULL是等价的,SELECT NVL('', 'x') FROM dual返回x。PostgreSQL里''不是NULL。这一条差异在数据迁移时特别容易产生脏数据。
我们在校验阶段发现不少表的某些字段两边值看起来不一致,查了半天才发现是源库存在空字符串,迁移时我们统一转换成了NULL,但业务代码里有用''判断的逻辑,导致行为变了。后来我们定了规范:Oracle到PostgreSQL迁移时,空字符串按照Oracle语义统一转NULL,同时业务代码里所有= ''的判断改为IS NULL。这个规范在改造阶段就要同步给开发团队。
5.5 分区表迁移的正确姿势
Oracle的分区表迁移到PostgreSQL有几种方案:用PostgreSQL原生分区表(基于声明式分区),或者用继承分区,或者用pg_partman这类扩展。我们的经验:如果分区数量不大(几十到几百个),用原生声明式分区就够了;如果分区数量上千,原生分区的元数据管理会有压力,可以考虑用pg_partman管理自动建分区。
迁移时的一个常见坑:Oracle分区表的全局索引和分区索引,在PostgreSQL里没有完全对应的概念,需要根据业务SQL的过滤条件重新设计索引策略。不要照搬Oracle的索引结构,否则查询性能会很差。
5.6 序列迁移的断档问题
Oracle的SEQUENCE不保证连续,PostgreSQL的SEQUENCE也一样。但两边序列的CACHE和INCREMENT配置不同步可能导致ID冲突或断档。我们迁移时把Oracle序列的LAST_NUMBER和INCREMENT_BY记录到PostgreSQL序列的同名参数上,还要考虑增量同步阶段两边应用各拿各的序列号,等切换后PostgreSQL序列起始值必须比Oracle当前值大很多,否则切过来后新生成的ID可能和同步回来的历史数据撞车。
这个问题真的很隐蔽,但如果爆了就是主键冲突直接拖垮切换。建议切换前用脚本检查所有序列的当前值,确保PostgreSQL侧的值远高于Oracle侧。
5.7 常见问题速查表
| 问题现象 | 常见原因 | 排查/处理办法 |
|---|---|---|
| 校验时哈希不一致 | 字段拼接顺序不一致、NULL/空串没统一、浮点精度差异 | 统一拼接规则,字段值做标准化 |
| 增量同步延迟持续增大 | Kafka消费能力不足、目标库应用能力差、大事务卡住 | 增加消费者并行度,排查大事务,必要时手动补数 |
| 切换后应用报驱动错误 | JDBC连接串或驱动版本不兼容 | Postgres JDBC驱动与Oracle JDBC驱动用法不同,检查URL参数、schema设置 |
| 存储过程执行失败 | PL/SQL和PL/pgSQL语法差异 | 人工改写,重点处理游标、动态SQL、异常处理 |
| 查询性能严重下降 | 统计信息未收集、索引缺失、优化器差异 | 切换后立即执行ANALYZE,根据执行计划补索引 |
| 回退时反向同步失败 | 类型映射不一致、主键冲突、循环依赖 | 提前演练,建立主键冲突处理规则,类型映射表要用同一份 |
我自己走完这个项目后最深的体会是:数据库迁移拼的不是单点技术有多强,而是流程管理和风险把控。这三个目标——低中断、可校验、可回退——每一个都对应着一整套方案和工具链,而它们之间又互相影响。建议准备动手的朋友,先把你现有的停机窗口、数据量、业务容忍度都摆出来,用这些边界条件去倒推每一步该怎么做。方案设计得越细,执行的时候才越稳。再补充一点:迁移过程中所有脚本和命令,一定要先在小环境里完整演练一遍,带上真实数据量的1%到5%压测一下性能,别等到正式切换才发现导出速度、校验耗时、增量延迟这些数字完全不可控。