ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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字节限制超长必须转CLOBnumeric / decimalNUMBER(p,s)PG的numeric无精度限制Oracle建议定义明确精度否则后续索引和查询性能不好把控booleanNUMBER(1)应用层要约定用1/0表示不能直接用true/falsetimestampTIMESTAMP(6)PG默认微秒精度Oracle默认秒级注意PG源字段可能有微秒值dateDATE这是个大坑PG的date只有日期没有时分秒Oracle的date是带时分秒的迁移后SQL行为会变化json / jsonbCLOB 或 Oracle JSON简单存储就CLOB如果业务需要在数据库里做JSON字段过滤再评估转Oracle JSONbyteaBLOB二进制数据导出时要转base64不能直接进CSVuuidVARCHAR2(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结果是NULLOracle中运算是a这个差异会导致很多拼接类SQL在迁移后结果不一致。凡是涉及||拼接的SQL最好把字段包一层COALESCE(field, )。函数差异NVL在PG里没有要用COALESCESYSDATE在PG里没有要用NOW()或CURRENT_TIMESTAMPDECODE在PG原生不支持要用CASE WHEN改写。伪表Oracle的SELECT 1 FROM DUALPG是SELECT 1不需要FROM。如果两边都要兼容可以在PG里建一个dual视图。序列Oracle的seq.NEXTVALPG是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 --tablepublic.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()转换后要替换成SYSTIMESTAMPPG的CURRENT_TIMESTAMP同样替换。如果是业务写入的固定默认值比如0、1、字符串就直接转成Oracle写法。3.3 SQL*Loader导入与性能调优SQL*Loader是Oracle自带的高性能数据加载工具比逐条INSERT快好几个数量级。控制文件写法分享下OPTIONS (SKIP0, DIRECTTRUE, ROWS5000, PARALLELTRUE) 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) )几个关键点DIRECTTRUE走直接路径加载绕开undo日志性能提升非常明显但导入期间表不能被其它会话修改要在停机窗口内执行。ROWS5000是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/passwordorcl controlmaterial.ctl logmaterial.log badmaterial.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 割接切换步骤割接时我们按照下面这个顺序操作前一周做模拟割接全量增量完整走一遍记录每一步耗时。正式割接当天先停应用写操作业务侧进入只读模式。停写之后把PG上从上次全量导出时间点开始积累的增量数据通过Debezium消费到Oracle。等Oracle端行数和PG关键数据对齐后切换数据库连接配置。验证通过后应用重新放开写操作。这里有个小经验切换后不要立刻删掉PG至少要保留两到四周的“回退窗口”。万一Oracle侧跑了一个多星期才发现严重问题至少还能切回去。应用层面提前做好双数据源配置数据源切换做成配置项而不是改代码。回退方案平时大家都不愿意花时间写真出事的时候就是救命稻草。把PG和Oracle的切换脚本、验证SQL、回退脚本都准备好放版本库这个时间花得值。5. 迁移后的常见问题与排查5.1 字符集与中文乱码这次没碰到但这项工作里最常遇到的问题就是它。PG导出默认UTF-8Oracle库如果创建时选了ZHS16GBK直接导入就会出现编码错乱。排查方法简单粗暴导入完成后随机抽10条中文记录看展示是否正常。如果乱码优先检查两端NLS_LANG设置和CSV文件编码。Linux下用file命令确认file -bi /data/migration/material.csv输出类似text/plain; charsetutf-8如果不是UTF-8就先用iconv转一次iconv -f UTF-8 -t GBK material.csv material_gbk.csv5.2 空字符串与NULL隐蔽的语义陷阱这个必须单独拿出来讲太容易踩了。PG里不等于NULLOracle里本身就是NULL。迁移一开始我们没注意结果发现原来PG里某些字段是空字符串的记录迁到Oracle后查IS NULL时被查出来了下游报表数据对不上。解决方法其实是在转换脚本里统一规则。如果业务里空字符串和NULL语义不同比如“未填写”和“没有值”是两码事就必须在迁移脚本里把空字符串转成业务约定的值比如EMPTY或者N/A而不是让它变成NULL。如果语义相同就无所谓统一成NULL即可。5.3 大表导入性能优化导入订单流水这种2亿行的表时我们第一次用默认配置导入花了接近3小时。后来调整了几处参数效率提升非常明显导入前先去掉表上的索引和约束等数据导入完成后再重建。索引在导入过程中会产生大量redo和索引分裂去掉之后速度成倍提升。使用DIRECTTRUE和PARALLELTRUE并行度设为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迁移的朋友一点参考。
返回列表