news 2026/9/13 10:38:36

PostgreSQL到Oracle迁移全流程实战:类型映射、增量同步与踩坑记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL到Oracle迁移全流程实战:类型映射、增量同步与踩坑记录

最近刚配合一个兄弟团队,把生产环境的物料主数据从PostgreSQL 14迁到了Oracle 19c,整个流程从方案评审到正式割接用了三周左右。这几年做数据迁移,MySQL迁PG、SQL Server迁Oracle都干过,但PostgreSQL到Oracle这种组合,坑是真的多:类型语义对不上、空字符串处理逻辑相反、分页写法完全两个套路、连Oracle时的监听报错也来凑热闹。网上专门讲这个方向的资料比较散,大多只讲“用某个工具导入导出”就结束了,真正干活时会遇到的各种边角问题很少有人写全。这篇文章把整个迁移过程的思路、工具选型、具体步骤、踩过的坑全部整理出来,给准备做类似工作的朋友一个可参考的完整路线。

1. 迁移前的整体设计与方案选型

1.1 为什么会做PG到Oracle的迁移

先聊下这次项目背景。这家公司的业务系统原先跑在PostgreSQL上,后来集团统一数据平台要求核心业务系统必须接入Oracle,我们接到的任务是把业务库里的物料主数据、库存快照、订单流水这些核心表整体迁到Oracle 19c。

说实话,PostgreSQL本身并不弱,但在企业级生态里,Oracle的历史存量太大。很多集团型公司、制造业头部企业、金融机构,内部的数据库标准就是Oracle,导致“非Oracle不上”的采购约束很常见。还有一类场景是反向的——某个商业软件产品只支持Oracle,公司买了产品之后必须把业务数据从自建PG搬进去。

不管哪种场景,核心问题都一样:怎么把数据平滑迁过来,并且尽可能少改应用代码。如果只是把数据“倒”过去,那太简单了,真正麻烦的是Oracle和PostgreSQL在SQL语义、事务行为、内置函数上的差异。所以迁移方案的制定,不能只看数据拷贝,还要考虑应用适配和后续增量同步。

1.2 迁移工具选型与方案取舍

数据迁移方案我习惯拆成两条线看:全量历史数据搬迁 + 增量变更同步。全量解决“存量”,增量解决“割接期间的变更”,两条线缺一条,业务都没法平稳切换。

全量迁移这块,可选的工具不少,我整理了一个对比:

工具适用场景优点缺点
pg_dump + SQL*Loader/外部表数据量可控、表结构不极端复杂免费、每步可控、定位问题容易需要自己处理类型映射,表多时工作量大
Oracle SQL Developer迁移工作台中小型项目自动做类型映射和DDL转换复杂对象支持一般,对PG版本兼容性有限
Kettle/DataX异构数据库、字段多可视化、支持类型转换超大表性能不理想,配置繁琐
OGG (Oracle GoldenGate)有持续增量同步需求增量能力强、准实时配置重、license成本高

最终我选了“pg_dump导出结构 + psql分批导出CSV + Python清洗脚本转换 + Oracle SQL*Loader批量导入”这个组合。为什么这么选?一是这次的数据量虽然到了亿级,但核心表只有20多张,属于典型的“表少、行多”场景;二是项目没有采购OGG的预算,增量同步准备另走技术方案;三是这个组合的每一步都能人工控制,出了问题能定位到具体环节,不像某些黑盒工具,卡住了都不知道数据在哪个环节变的样。

增量那块,我们评估了OGG和Debezium,最后用了Debezium + Kafka + 自研消费程序。这里面有个取舍:OGG虽然和Oracle生态结合最紧密,但配置复杂度高、需要单独的license,对一个没有购买预算的项目来说不现实。Debezium基于PostgreSQL的逻辑复制,依赖Kafka做消息管道,灵活性更高,代价是链路更长、需要自己处理消费端的数据应用逻辑。具体后面第4部分展开讲。

注意:手工做类型映射脚本,一定要先写一份“DDL转换规则文档”,把PG的类型、约束、默认值、注释对应到Oracle的写法。20张表看着不多,但每张表几十个字段,不提前定好规则,改到第10张表就开始混乱,第15张表就会出低级错误。

2. 迁移前的差异盘点

2.1 数据类型映射,这是最容易翻车的地方

这个部分是整个迁移的基本功,也是后续所有问题的源头。PostgreSQL的数据类型和Oracle的差异,不是简单的改名,部分类型语义完全不同。

我先把这次用到的映射表放出来:

PostgreSQLOracle备注
serial / bigserialNUMBER(10) / NUMBER(19) + SEQUENCE + TRIGGERPG自增是序列默认值,Oracle 12c以后虽然支持identity列,但迁移大批量数据时显式插入ID会导致identity序列不同步,用序列+触发器更可控
varchar(n)VARCHAR2(n CHAR)PG的n是字符数,Oracle默认按字节计算,建表时明确加CHAR防止中文边界问题
textCLOBPG的text无限长,Oracle的VARCHAR2在SQL语句中受4000字节限制,超长必须转CLOB
numeric / decimalNUMBER(p,s)PG的numeric无精度限制,Oracle建议定义明确精度,否则后续索引和查询性能不好把控
booleanNUMBER(1)应用层要约定用1/0表示,不能直接用true/false
timestampTIMESTAMP(6)PG默认微秒精度,Oracle默认秒级,注意PG源字段可能有微秒值
dateDATE这是个大坑,PG的date只有日期没有时分秒,Oracle的date是带时分秒的,迁移后SQL行为会变化
json / jsonbCLOB 或 Oracle JSON简单存储就CLOB,如果业务需要在数据库里做JSON字段过滤,再评估转Oracle JSON
byteaBLOB二进制数据,导出时要转base64,不能直接进CSV
uuidVARCHAR2(32)也可以映射成RAW(16),但可读性差,我选了VARCHAR2(32)
inet / cidrVARCHAR2(45)用字符串存,应用层自己解析

这张映射表不是拍脑袋定的,每一条都对应一个具体测试。比如serial到Oracle的映射,我在测试库先建了一张小表,分别试了identity列和序列+触发器两种方案。最后选序列+触发器的原因很现实:Oracle的identity列在显式插入ID时不会自动推进底层序列,等后续业务插入时就会碰到主键冲突。序列+触发器虽然多一个数据库对象,但能保证所有插入路径都拿到正确的ID。

2.2 SQL语法与内置函数差异,应用改造的依据

数据迁移不只是搬数据,搬完之后应用还要能跑。所以SQL差异必须提前整理成一份“应用改造手册”,这里列几个这次踩到的重点:

  • 分页:PG是LIMIT #{limit} OFFSET #{offset},Oracle 12c以上可以用OFFSET ... ROWS FETCH NEXT ... ROWS ONLY,老代码可能还在用ROWNUM。改成ROWNUM要特别注意排序和分页的嵌套顺序,先排序再取ROWNUM,顺序反了分页结果就是乱的。
  • 空字符串:PG里''和NULL是两个概念,Oracle里''会被自动当成NULL。这个差异非常隐蔽,如果源库某些字段存在空字符串,迁移后查IS NULL会查出原来并非空的数据,报表对账直接就出问题。
  • 字符串拼接:PG和Oracle都支持||,但PG中'a' || NULL结果是NULL,Oracle中运算是'a',这个差异会导致很多拼接类SQL在迁移后结果不一致。凡是涉及||拼接的SQL,最好把字段包一层COALESCE(field, '')
  • 函数差异:NVL在PG里没有,要用COALESCESYSDATE在PG里没有,要用NOW()CURRENT_TIMESTAMPDECODE在PG原生不支持,要用CASE WHEN改写。
  • 伪表:Oracle的SELECT 1 FROM DUAL,PG是SELECT 1,不需要FROM。如果两边都要兼容,可以在PG里建一个dual视图。
  • 序列:Oracle的seq.NEXTVAL,PG是NEXTVAL('seq_name'),应用层用序列时要改SQL写法。

这些差异如果只是做数据迁移可能感知不强,但应用一旦切换连接,所有SQL都会暴露出来。我的建议是:迁移项目里必须安排一个“SQL兼容性扫描”环节,把应用代码里所有SQL、存储过程、定时任务脚本拉出来,一条条过。漏掉任何一条,上线后都是生产事故。

2.3 对象、权限与配置差异

除了表和SQL,还有几个容易被忽略的地方:

  • Schema体系:PG是“库-模式-表”,Oracle是“实例-用户-表”。一个PG库下有多个schema,映射到Oracle时通常是一个schema对应一个Oracle用户。迁移前要先确认好哪个PG schema对应哪个Oracle用户、哪个表空间,否则建表时会发现对象建错了地方。
  • 索引:PG的部分索引(partial index)、表达式索引、GIN索引在Oracle里要转换。部分索引可以改成Oracle的CREATE INDEX ... WHERE ...形式,GIN索引针对全文或JSON,要评估业务是否真正依赖,否则可以直接删掉。
  • 触发器:PG和Oracle触发器语法不同,但迁移时更要紧的是逐个评审业务触发器是否还需要保留。很多触发器是历史遗留,可能已经失去作用,趁迁移做一次“减负”是个好机会。
  • 权限和同义词:Oracle的权限体系比PG严格,迁移后要给业务账号单独赋表、序列、视图的权限。很多应用连接Oracle时会直接用不带schema前缀的表名,如果账号对应的schema不是表所在schema,必须建同义词或者让应用带上schema前缀。

3. 全量迁移实操记录

3.1 导出准备与执行

这次迁移的路线是“导出CSV + 清洗脚本 + SQLLoader导入”。CSV格式通用,出问题好定位,而且SQLLoader对大批量文件处理效率很高。

先说导出。PG导出CSV,我习惯用psql的\copy命令,注意是\copy而不是服务端的COPY。两者区别在于\copy是在客户端执行,导出的文件直接写到本地,不需要数据库超级用户权限,权限控制上更安全可控。

# 导出单表,自定义分隔符,防止字段里出现默认逗号 psql -h pg-server -U pguser -d sourcedb \ -c "\copy public.material TO '/data/migration/material.csv' WITH (FORMAT CSV, DELIMITER '|', HEADER false, NULL '\\N')"

这里有个关键参数:NULL '\\N'。PG在COPY导出时默认把NULL值写为\N,如果不显式指定,导出的CSV里NULL和空字符串会混在一起,导入阶段就很难区分了。我们提前确认了源库字段里没有出现\N这种异常值,才放心用这个标记。

对于大表,一次性导出整份CSV容易出现内存问题,我们按主键区间做分批导出。比如订单流水按月份切:

psql -h pg-server -U pguser -d sourcedb \ -c "\copy (SELECT * FROM public.order_flow WHERE create_time >= '2025-01-01' AND create_time < '2025-02-01') TO '/data/migration/order_flow_202501.csv' WITH (FORMAT CSV, DELIMITER '|', NULL '\\N')"

分批导出的好处是单个文件大小可控,导入Oracle时可以并行处理,性能更好。我们一般把文件控制在5GB以内,这是SQL*Loader direct路径模式比较稳的分界点。

3.2 从DDL转换到目标库建表

导出数据的同时,结构也要同步处理。PG的结构可以通过pg_dump拿到,但Oracle不认PG的语法,需要按第2部分的映射规则转换。

pg_dump -h pg-server -U pguser -d sourcedb --schema-only --table=public.material -f /data/migration/material_schema.sql

拿到这个文件后,我们不是直接手写Oracle DDL,而是写了一个Python解析脚本,把常见类型做替换。脚本逻辑不复杂,核心就是把字段类型、默认值、注释按映射表替换,遇到复杂的表达式再人工介入。为什么要写脚本而不是手写?20张表、每张表50个字段,手写出错率太高,脚本至少能保证同类处理一致,后续修改也方便。

转换后的建表语句大致长这样:

CREATE TABLE admin.material ( id NUMBER(19) NOT NULL, material_code VARCHAR2(50 CHAR) NOT NULL, material_name VARCHAR2(200 CHAR), category_id NUMBER(10), status NUMBER(1) DEFAULT 1 NOT NULL, ext_info CLOB, create_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP, update_time TIMESTAMP(6), CONSTRAINT pk_material PRIMARY KEY (id) ); COMMENT ON TABLE admin.material IS '物料主数据表'; -- 索引、序列、触发器单独创建

注意几个转换细节:PG的默认值如果用的now(),转换后要替换成SYSTIMESTAMP;PG的CURRENT_TIMESTAMP同样替换。如果是业务写入的固定默认值,比如0、1、字符串,就直接转成Oracle写法。

3.3 SQL*Loader导入与性能调优

SQL*Loader是Oracle自带的高性能数据加载工具,比逐条INSERT快好几个数量级。控制文件写法分享下:

OPTIONS (SKIP=0, DIRECT=TRUE, ROWS=5000, PARALLEL=TRUE) LOAD DATA INFILE '/data/migration/material.csv' INTO TABLE admin.material FIELDS TERMINATED BY '|' TRAILING NULLCOLS ( id, material_code, material_name, category_id "TO_NUMBER(:category_id)", status, ext_info, create_time "TO_TIMESTAMP(:create_time,'YYYY-MM-DD HH24:MI:SS.US')", update_time "TO_TIMESTAMP(:update_time,'YYYY-MM-DD HH24:MI:SS.US')" )

几个关键点:

  • DIRECT=TRUE走直接路径加载,绕开undo日志,性能提升非常明显,但导入期间表不能被其它会话修改,要在停机窗口内执行。
  • ROWS=5000是direct路径下一批提交的记录数,太大会消耗PGA内存,太小性能差,实测5000到10000比较合适。
  • TRAILING NULLCOLS很关键,如果CSV某行末尾字段为空,SQL*Loader默认会报错,加上这个参数会把缺失列补成NULL。
  • 时间字段:PG导出的时间格式默认是2025-06-01 12:30:45.123456这种带微秒的,Oracle的TO_TIMESTAMP.US匹配微秒。建议清洗脚本统一格式化,免得导Oracle时各种解析报错。

导入执行:

sqlldr admin/password@orcl control=material.ctl log=material.log bad=material.bad

执行完必须看log文件里的内容,重点看Rows successfully loaded这一行,和PG源表行数对比。bad文件如果有内容,说明有记录被拒,要一条条排查原因。

3.4 数据一致性校验

迁移最怕的就是“看着行数一样,实际数据不对”。所以校验不能只数行数,我们这次做了三层校验。

第一层,行数比对。导出前在PG统计每个表的COUNT,导入后Oracle再COUNT一次,数字必须一致。如果表很大,COUNT(*)也慢,可以分批文件的记录数总和代替。

第二层,关键字段聚合校验。对金额类字段比较SUM,对时间字段比较MIN和MAX,对状态字段比较每个状态值的COUNT。两边各算一遍,不一致就说明某条记录有问题。

第三层,抽样明细比对。写一个对比脚本,两边各取主键ID同一批样本,用MD5把整行拼成字符串,然后对比哈希。PG端用MD5(CAST(row AS TEXT)),Oracle端用LOWER(STANDARD_HASH(row, 'MD5')),拼接格式要统一。这条测试最费劲,但能发现列错位、精度丢失的问题。

注意:数据库字符集务必在迁移前统一确认。我们这趟还算顺利,因为两边都是UTF-8。如果源库是UTF8、目标库是GBK,中文会乱码,转换脚本里必须加入字符集转换逻辑,或者用iconv先处理文件。

4. 增量同步与割接保障

4.1 增量方案选择,OGG之外的可行路线

全量迁移是基础,但业务不能停。从PG切到Oracle,割接期间会有新数据产生,需要一套增量同步方案把变化数据搬过去。

OGG是最标准的方案,支持从PostgreSQL抽取数据到Oracle,但配置复杂,而且OGG for PostgreSQL需要单独采购。没有预算的项目,这次我们用了Debezium。Debezium通过PostgreSQL逻辑复制插件(pgoutput或wal2json)捕获变更,输出到Kafka,消费端程序再把变更应用到Oracle。

这个链路的前期工作量主要在Kafka集群和消费程序的开发上。优点是灵活,能对数据做二次处理,Oracle端的字段映射也能自定义。缺点是引入中间件后链路变长,任何一个环节出问题,积压数据都要花时间追平。所以割接前一定要反复做压测,让增量积压的时间维持在可控范围。

4.2 割接切换步骤

割接时我们按照下面这个顺序操作:

  1. 前一周做模拟割接,全量+增量完整走一遍,记录每一步耗时。
  2. 正式割接当天,先停应用写操作,业务侧进入只读模式。
  3. 停写之后,把PG上从上次全量导出时间点开始积累的增量数据,通过Debezium消费到Oracle。
  4. 等Oracle端行数和PG关键数据对齐后,切换数据库连接配置。
  5. 验证通过后,应用重新放开写操作。

这里有个小经验:切换后不要立刻删掉PG,至少要保留两到四周的“回退窗口”。万一Oracle侧跑了一个多星期才发现严重问题,至少还能切回去。应用层面提前做好双数据源配置,数据源切换做成配置项,而不是改代码。回退方案平时大家都不愿意花时间写,真出事的时候就是救命稻草。把PG和Oracle的切换脚本、验证SQL、回退脚本都准备好,放版本库,这个时间花得值。

5. 迁移后的常见问题与排查

5.1 字符集与中文乱码

这次没碰到,但这项工作里最常遇到的问题就是它。PG导出默认UTF-8,Oracle库如果创建时选了ZHS16GBK,直接导入就会出现编码错乱。

排查方法简单粗暴:导入完成后,随机抽10条中文记录看展示是否正常。如果乱码,优先检查两端NLS_LANG设置和CSV文件编码。Linux下用file命令确认:

file -bi /data/migration/material.csv

输出类似text/plain; charset=utf-8,如果不是UTF-8,就先用iconv转一次:

iconv -f UTF-8 -t GBK material.csv > material_gbk.csv

5.2 空字符串与NULL,隐蔽的语义陷阱

这个必须单独拿出来讲,太容易踩了。PG里''不等于NULL,Oracle里''本身就是NULL。迁移一开始我们没注意,结果发现原来PG里某些字段是空字符串的记录,迁到Oracle后查IS NULL时被查出来了,下游报表数据对不上。

解决方法其实是在转换脚本里统一规则。如果业务里空字符串和NULL语义不同,比如“未填写”和“没有值”是两码事,就必须在迁移脚本里把空字符串转成业务约定的值,比如'EMPTY'或者'N/A',而不是让它变成NULL。如果语义相同,就无所谓,统一成NULL即可。

5.3 大表导入性能优化

导入订单流水这种2亿行的表时,我们第一次用默认配置导入花了接近3小时。后来调整了几处参数,效率提升非常明显:

  • 导入前先去掉表上的索引和约束,等数据导入完成后再重建。索引在导入过程中会产生大量redo和索引分裂,去掉之后速度成倍提升。
  • 使用DIRECT=TRUEPARALLEL=TRUE,并行度设为4。
  • 数据按主键哈希或时间拆分,分批导入。
  • 导入过程中临时关闭归档日志,这个需要DBA确认,测试环境验证后再操作。

最终总耗时从3小时降到了50分钟左右。当然这是特定环境下的经验,服务器配置、网络、磁盘类型都会影响结果,但方向是通用的:先数据、后索引、再约束。

5.4 Oracle监听与连接类报错

迁移后应用连Oracle时,经常会碰到两类和连接相关的问题,虽然不是数据层问题,但迁移后第一天最容易因此被电话吵醒。

第一类是ORA-28500或ORA-28547,通常和Oracle Net配置有关。常见原因包括:监听器的SID或SERVICE_NAME和JDBC连接串不一致、Oracle Net的版本和数据库版本不匹配。排查顺序是:先用lsnrctl status看监听是否起来,再用tnsping测试,最后检查tnsnames.ora里的SERVICE_NAME和数据库的GLOBAL_DBNAME是否一致。

第二类是监听服务无法启动,执行lsnrctl start时提示TNS-12541之类的错误。多数是本机hosts配置问题或端口被占用。Windows环境下常见于Oracle安装不干净导致注册表残留,处理办法是按官方文档完整卸载后重装,或者修改listener.ora换端口。

这些问题虽然不属于数据迁移本身,但在迁移项目里出现频率极高。建议把Oracle环境的检查项,包括监听、实例状态、权限、表空间大小,整理成一份上线前巡检清单,割接前逐项过一遍。

5.5 PG特有类型的特殊处理

最后补充一些动手过程中的细节:

  • jsonb转CLOB后,Oracle端如果只是存原文由应用解析,CLOB方式没问题。如果应用要在库里直接做JSON字段过滤,就要评估改成Oracle JSON列,或者把过滤逻辑挪到应用层。
  • uuid导出为字符串后注意大小写。PG的UUID文本是小写,Oracle如果用RAW(16)接收,还需要按16进制解码。建议统一用VARCHAR2(32)存储,应用层该干嘛干嘛。
  • bytea二进制导出时要转成base64文本,导入端再解码。直接把原始字节写CSV里,会被SQL*Loader当成文本处理,文件容易损坏。
  • PG的money类型尽量不要用,Oracle没有直接对应类型,一般转成NUMBER(12,2)。

最后说点实在的。这次PG到Oracle迁移能顺利走完,我觉得靠的是两个习惯:一是在迁移前把差异盘点做扎实,每张表、每条SQL都过了一遍,没有抱着“先迁过去再说”的心态;二是割接方案里预留了充分的回退路径,时间再紧也不压缩验证环节。数据迁移这件事,本质上是个工程问题,不是靠运气就能成的。任何一处“应该没问题”的侥幸,最后几乎都会变成生产事故。提前把计划做细、把每一步的检查和验证做扎实,剩下的就只是耐心执行。希望这份过程总结,能给正在做或者准备做PG到Oracle迁移的朋友一点参考。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 10:37:34

Maven从配置到实战:依赖管理、镜像加速与IDEA集成避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 10:35:48

HTML基础语法入门:从标签到网页骨架的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 10:35:45

Available tools

Available tools 【免费下载链接】marimo A reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor. 项目地址: https:…

作者头像 李华