2026/10/14 22:14:38

制造ERP数据库国产化迁移实践:从Oracle到KBASE的全程复盘

制造ERP数据库国产化迁移实践:从Oracle到KBASE的全程复盘 去年这个时候我接到一个制造行业ERP全栈国产化的项目某汽车零部件制造集团的ERP系统要整体替换数据库层从原来的商业数据库换成国产的KBASE数据库。这家集团的ERP覆盖生产计划、物料、库存、成本核算、质量管理属于典型的重度制造型ERP报表几千张存储过程几百个还牵扯多个工厂的跨库数据汇总。项目落地在华东某制造重镇前后做了四个多月。这篇文章就把整个过程里值得讲的东西拆开聊一聊尤其是数据库国产化过程中那些“看着简单、实际绕人”的细节。如果你正准备做类似替换或者正在评估国产数据库到底扛不扛得住制造业核心系统这篇文章应该能帮上忙。1. 立项背景一套运行十年的ERP为什么要动数据库这层地基1.1 项目原貌与替换范围先说说这套系统原来的样子。源库是一套商业关系型数据库版本停留在一个很稳定的老版本两台物理机做主备整库大小约3.2TB高峰期日志量增长很快。应用层是Java 某商业中间件几乎所有报表和月末批处理都依赖一套庞大的存储过程体系。系统本身跑得很稳业务部门已经用了十年很多关键岗位的操作习惯都建立在它的界面上。为什么还要动最直接的原因是技术栈独立选型的需要。企业要求底层基础软件逐步实现本地化部署包括操作系统、中间件、数据库、办公套件这些都要换成可在国内持续获得支持的方案。但真正决定项目范围的是“全栈”这两个字。一开始我们只想替换数据库后来发现应用运行环境不换数据库层的问题是解决不彻底的比如驱动兼容性、连接池适配、JDBC行为差异都会和中间件绑定在一起。于是顺理成章地把应用部署环境也换成了某国产Linux发行版和国产中间件。这个替换的难点在于目标不是“能跑起来”而是“长期稳定地跑”。制造企业ERP是核心中的核心停机窗口极其有限。业务部门给我们的要求很朴素“你们换归换但月底算成本的时候不能慢车间扫码不能卡。”这句话后来成为所有验收指标的原点。1.2 数据库选型为什么最终选了KBASE选型阶段我们对比了三类候选库原库继续续约、某国产分布式数据库甲、某国产集中式数据库乙以及最后中标的KBASE。表格放这里方便大家看当时的关键判断维度比较维度原商业库KBASE V8候选库甲候选库乙Oracle语法兼容度原生高支持包/存储过程/触发器中分片语法优先中低自研SQL方言多制造行业已有案例无数较多集中在装备制造/汽车零部件金融、政务案例多偏OA、门户类轻业务高可用组件RAC/DataGuard主备集群自动切换分布式多副本主备切换依赖外围脚本原厂支持响应商务流程长原厂团队可驻场远程为主远程为主迁移工具成熟度不需要自带迁移工具支持对象转换有工具但版本限制多较弱基本靠手工最终选KBASE核心原因是它对Oracle语法兼容度最高。这个兼容性意味着存储过程和业务SQL的改造量相对可控。第二个原因是它的高可用架构是集中式主备集群不是一上来就是分布式分片。制造ERP里大量业务是单表单行短事务分布式架构在这种场景下反而会增加事务复杂度。第三个原因是原厂支持响应快项目期间给了驻场支持这在迁移类项目里太重要了。这里想多说一句很多团队选型时把注意力放在“性能跑分”上但我们的经验是迁移项目里最贵的成本不是机器性能而是应用改造和排错的时间。一个数据库和旧系统语法兼容度差哪怕单机性能再高也扛不住几千条SQL逐条重写的工程量。选型一定要先算总账。2. 上线前最费劲的环节存量对象迁移与SQL方言改造2.1 盘点需要搬家的数据库对象迁移前的第一步不是写脚本而是盘家底。我们先把源库里所有对象数量清了一遍再按改造复杂度分优先级。结果如下对象类型数量改造复杂度说明表4,200低数据类型和DDL语法需要转换索引6,800低部分函数索引需要重写存储过程/函数/包860高PL/SQL语法差异集中地触发器120中注意触发顺序、语句级/行级语义序列350中自增属性、缓存大小、NEXTVAL行为视图480中复杂SQL和连接语法物化视图12高新库有自己的物化视图定义需重写定时任务20中作业调度机制不同一个很反直觉的结论一开始我们觉得4200张表的DDL转换最耗时后来发现DDL大部分能用工具自动转真正吃时间的是860多个存储过程。存储过程里到处是业务规则一条条检查、改写、编译、比对结果比单纯搬数据麻烦得多。2.2 存储过程与包从PL/SQL到KBASE-SQL的改写套路举一个典型的例子。源库有一段更新物料需求计划状态的存储过程逻辑不复杂但写法非常“源库风格”CREATE OR REPLACE PROCEDURE sp_update_mrp_status( p_plan_id IN NUMBER, p_status OUT VARCHAR2 ) IS v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM mrp_order WHERE plan_id p_plan_id; IF v_count 0 THEN UPDATE mrp_order SET plan_status p_status, update_time SYSDATE WHERE plan_id p_plan_id; IF SQL%ROWCOUNT 0 THEN RAISE_APPLICATION_ERROR(-20001, no data updated); END IF; ELSE p_status : NOT_FOUND; END IF; END;换成KBASE之后最理想的情况是这段代码原样就能编译。实际上KBASE对NUMBER、VARCHAR2、SYSDATE、SQL%ROWCOUNT、RAISE_APPLICATION_ERROR这些都有兼容所以这类过程大概率不用动。但我们的项目里真正要改的是另外几类情况第一包内变量引用方式。源库里包体可以直接引用包头定义的全局变量新库对包变量的作用域解析在某些嵌套场景下会报“变量不存在”必须显式加包名前缀。第二动态SQL的绑定变量写法。源库用EXECUTE IMMEDIATE ... USING ...KBASE也支持但涉及RETURNING INTO和批量绑定的场景游标和集合类型的声明方式有差异需要改写。第三数据类型默认精度。源库里NUMBER没写精度时能存任意长度数字迁移工具经常映射成NUMERIC(38)大数计算溢出时表现不一样。第四字符函数的边界行为。SUBSTR、INSTR、NVL这些大部分一致但涉及多字节字符和排序规则时结果集顺序会有细微差别这点后面单独说。我们整理了一个“改动清单”把八百多个存储过程按“不用改、小改、大改”三档分类结果大约是55%完全不用动30%需要调整参数或变量声明15%涉及逻辑重构。这个比例其实比预期好但在15%里踩的坑占了整个项目一半以上的排错时间。2.3 那些容易藏在业务代码里的“隐性方言”存储过程只是第一站。Java应用里、报表SQL里、ORM的XML里都藏着方言这些才是最容易被忽略的。首先是分页。源库的经典写法是用ROWNUM包一层MyBatis分页插件会针对不同数据库自动生成分页SQL。换库后需要重新配置方言类型。KBASE同时兼容源库和MySQL两种分页习惯但我们在项目里做了统一所有分页SQL改成LIMIT写法消除两层分页嵌套导致的执行计划偏差。其次是序列。源库里很多表的主键靠序列生成KBASE支持序列但默认CACHE值偏保守。生产环境第一次跑批量导入时大量并发请求NEXTVAL数据库出现明显的序列等待事件。修改方法很简单把序列CACHE从默认值调到200以上。第三是空字符串语义。这是最容易出业务bug的地方。源库里空字符串会被当作NULL处理而新库遵循标准SQL空串就是空串。很多“查不到数据”的诡异问题最后定位都是因为应用在某个逻辑分支上传入了空串老库直接走NULL判断新库却带入了比较条件。这一条我们做了全量扫描把涉及或者的逻辑全部确认了一遍。第四是日期格式。源库的TO_DATE解析比较宽松TO_DATE(2024-1-5,YYYY-MM-DD)能过新库某些情况下要求严格匹配格式串。最稳的做法是在应用层统一用YYYY-MM-DD HH24:MI:SS格式数据库端只做标准转换。最后是递归查询。制造业ERP里BOM多级展开、组织架构树很常见。源库里用CONNECT BYKBASE也支持但遇到CONNECT BY和JOIN、WHERE混用的复杂语句优化器有时不把它识别成树形查询。我们有一条BOM递归SQL老库跑2秒迁移后第一次跑出30秒。后来改写成递归CTEWITH RECURSIVE写法耗时回到2.1秒。这个例子给的教训是语法兼容不等于性能兼容上线前一定要把核心递归语句逐条review。3. 迁移方案与技术选型的几个关键决策3.1 用官方迁移工具还是自研脚本KBASE官方提供了迁移工具能把大部分DDL和数据自动搬过去。我们评估后的策略是四个字混合并行。工具负责跑DDL转换、表结构迁移和全量数据导入同时我们自研三套脚本做辅助。第一套脚本是“对象比对脚本”在迁移完成后逐表、逐索引、逐视图比对结构定义输出差异清单。第二套脚本是“数据校验脚本”负责抽样和全量比对。第三套是“业务逻辑核对脚本”用SQL把核心模块的汇总数据重新算一遍和旧系统的日报表做交叉验证。为什么不全依赖官方工具因为迁移工具对常见场景很高效但遇到分区表、函数索引、多列外键、带特殊存储结构的表时容易出现转换不全或索引丢失。完全手工又不可能在有限窗口内完成。所以“工具先跑脚本兜底人工抽检”是这类迁移项目最务实的解法。这里有一个关键教训迁移工具生成的DDL一定要人工review不能直接执行。工具把源库的NUMBER映射成NUMERIC但部分列原本是NUMBER(12,2)工具却可能转成NUMERIC(38,4)虽然数据能放下但JPA生成的SQL里绑定参数类型可能不匹配导致索引失效。这种问题用行数校验查不出来但用执行计划能看到。3.2 数据一致性校验的笨办法数据量一大“数据有没有搬对”就成了最焦虑的问题。我们用了三层校验看着笨但每个都有不可替代的作用。第一层是行数统计。所有表逐表COUNT比对源库和目标库。注意大表COUNT在大数据量下很慢所以我们按表分区或抽样块来做对4亿行的流水表先比分区统计值再抽查明细。这一层能快速发现“表被搬漏了”或“迁移过程中源库又写了新数据”。第二层是校验和比对。把每张表的主键和关键列拼接成一个字符串计算MD5再把所有行的MD5聚合一次最后比对源库和目标库的聚合值。这层能发现行数一样但某列值被篡改或类型转换丢精度的问题。全表跑一次很耗时我们就拆成多线程并行每个线程负责一批表大概六个小时能跑完全库。第三层是业务对账。这里比较“制造业特色”库存总账和明细账的余额是否一致销售订单与发货单的勾稽关系是否成立工单领料数量与投料记录是否一致。行数和MD5都验证的是“每个字节都一样”业务对账验证的是“搬完之后业务逻辑还能闭环”。我们当时果然靠第三层发现了一个状态字段在转换时被工具改写了枚举值界面显示“已关闭”但状态码变成了另一个值导致月末结账时少算了一笔在途订单。这类bug只能靠懂业务的人盯着数据查出来。还有个小插曲我们靠MD5校验发现一张日志表在迁移后的第六个小时数据又变了。排查发现源库有个后台定时任务还会写历史表迁移时没有完全停干净。所以数据校验的窗口期必须放在所有外部写路径全部掐断之后否则校验永远对不上。3.3 字符集与排序规则最容易被低估的坑制造业ERP里的中文数据特别多物料描述、BOM备注、客户品名、质检结论全是中文。源库是GBK体系目标库我们设成UTF8。理论上转换过程很成熟但实际踩了三个坑。第一个是字段长度不够。VARCHAR2(20)在GBK里能存10个中文汉字在UTF8里一个汉字占三字节20字节只能存6个汉字。很多历史字段定义长度是按GBK算的迁移后插入时报“字符串截断”或“值太大”。我们把全库这类字段扫了一遍涉及几千个列统一按三到四倍长度扩容。这类问题在开发环境很难发现因为测试数据通常很短生产数据才有大量长文本。第二个是排序规则不一致。源库GBK排序按拼音规则目标库默认二进制排序按Unicode码点导致物料清单、客户列表的排序结果和原来完全不同。下游打印、导出Excel的顺序全变了业务部门一度以为是数据丢了。解决方法是给相关列显式指定COLLATE尽量逼近旧库的排序行为。这个细节在迁移方案里一定要提前规划不然后面返工量很大。第三个是JDBC日期格式的默认行为。应用连源库时连串里设置了NLS_LANG或日期格式参数换库后这些参数失效可能从“2024-05-01”变成“01-MAY-24”。我们最后统一在应用层用标准Java时间类型不在SQL里依赖数据库默认格式这才彻底解决。4. 压测与性能调优别让国产库“能跑”变成“能扛”4.1 压测模型制造业务最典型的混合负载很多国产数据库项目开发环境跑得很顺一到生产就翻车核心原因是压测模型没有建好。制造业ERP的负载不是单纯CRUD而是三类负载同时存在第一类是OLTP短事务。工单下达、领料、入库、扫码事务很轻、频率很高这类请求要求响应时间稳定在毫秒级。第二类是批处理作业。月末成本核算、MRP运算SQL复杂、IO密集、单个事务能跑几分钟CPU占用率很高。第三类是报表查询。大量历史数据汇总读多写少偶尔还有大表全扫描。我们当时用JMeter建了三个场景组场景A模拟600个在线作业用户场景B模拟50个并发批处理任务场景C模拟30个并发报表查询。总并发初期在1800左右后期逐步压到3000观察TPS和响应时间的p95/p99同时监控数据库侧的等待事件、连接占用率、CPU和IO。压测暴露的问题非常典型。第一个是批处理任务启动后CPU被打满在线作业的响应时间从80毫秒暴涨到2秒。第二个是几张核心大表的报表SQL没走索引全表扫描。第三个是连接池默认配置太小高峰时出现等待获取连接的超时。这三个问题分别对应新库参数、SQL索引和连接池都需要单独拆解。4.2 调优三板斧统计信息、索引、连接池调优顺序很重要先收集统计信息再看执行计划最后才动数据库参数。顺序反了后面所有优化都是在猜。统计信息这一块KBASE默认开启了自动收集但大表在批量导入后统计信息会过期。我们写了一个每日凌晨的定时任务对前一天发生大量DML的表重新收集统计信息-- 伪代码示意每日统计信息刷新逻辑 CALL DBMS_STATS.GATHER_TABLE_STATS(public, stock_onhand, cascade TRUE); CALL DBMS_STATS.GATHER_TABLE_STATS(public, mrp_requirement, cascade TRUE);索引调优是收益最大的一块。我们通过数据库的慢SQL日志和动态性能视图找出Top SQL逐条看执行计划。有一张库存流水表6000万行原来只靠单列索引加了一个组合索引(warehouse_id, item_id, trans_date)之后月度库存汇总报表从85秒降到3秒。这类优化思路和原库完全一致没有新东西但换库之后必须要重新做一遍因为统计信息分布和执行计划机制都有差异。连接池是被很多人忽略的调优点。Java应用用的连接池制造场景建议maxActive设置成“CPU核数×2再乘以应用实例数”不要贪多。我们实测连接池从200调到300TPS提升5%但数据库端连接数从300调到600TPS反而降了15%。原因很简单连接数超过临界值后数据库的线程上下文切换开销会吞掉并发收益。这个“先增后降”的临界点不同机器、不同负载都不一样压测里一定要扫出来。4.3 锁等待与死锁问题一次凌晨故障复盘上线前的某轮压测中凌晨两点大批量作业触发了死锁报警。日志显示两张表互相等待会话A先更新物料需求计划表再更新库存表会话B的操作顺序正好相反最终形成循环等待。这个现象很经典不是数据库引擎的bug而是应用层并发顺序不一致。解决办法是两件事第一把所有存储过程和Java服务里的表更新顺序统一成固定顺序从源头避免循环第二在应用端增加锁重试机制捕获死锁异常后回滚重试把偶发冲突的影响降到最低。另外还遇到一个锁等待问题车间扫码入库的短事务和月末成本核算的长事务会操作同一行库存记录长事务锁持有时间久导致扫码响应时间大幅上涨。这个问题数据库层解决不了最后是优化了成本核算SQL的逻辑让它先读快照再分批次更新缩短了锁的持有时间。压测阶段一定要模拟这种长事务和短事务的冲突否则上线后必炸。5. 正式割接双写方案、回退预案和一周试运行5.1 割接流程设计割接窗口选在周末两天整体节奏是“周四停业务、周五迁移、周末验证、下周一看高峰”。具体流程周四晚上停掉源库业务做最后一次全量备份。周五凌晨进行最终增量迁移和数据校验。周五白天到周六白天在目标库上做全功能冒烟测试覆盖核心业务链路包括销售订单创建、生产工单下达、领料、完工入库、库存查询、月度成本报表。周六晚上把应用正式切到新库之后一直盯到下周一的出货高峰结束。我们没有做长时间的“双写”原因很务实制造ERP的表关联复杂单据状态在多个表里同步更新双写一旦出现不一致追查和修复的成本比直接切换高得多。所以采用“短切换、长盯防”的策略切换窗口很短但切换后的深度监控要持续两周。回退预案方面源库环境保留两周所有数据文件、归档日志都不删除。如果新库出现不可接受的重大问题可以把应用切回源库再用最后一份增量记录做追平。这里必须提醒一句回退预案不能只写文档一定要演练。我们在周五做了一次真实回退演练从切回源库到业务恢复共三小时。虽然周五多加了三个小时班但团队心里有底了。5.2 割接当晚的检查清单与意外处理割接当晚的检查清单我按经验整理如下应用启动后先跑五条核心查询确认主链路是否通查看客户端连接数是否在预期范围盯后台任务调度是否按时触发所有定时任务要重新确认检查主备同步延迟是否归零验证所有数据库账号业务账号、只读报表账号、ETL账号数量多容易漏。当晚实际出了两个意外。第一个是报表中心部分账号连不上库原因是账号密码策略因字符大小写敏感设置变化导致验证失败。处理起来很快但排查时很吓人因为错误日志报的是“找不到用户”。第二个意外是凌晨四点一条批处理任务失败查日志发现是因为旧库和新库的作业调度重复执行了同一个任务产生了重复数据。我们把失败批次回滚重跑并统一把作业调度改到应用层禁止数据库内核自带的定时任务继续使用。这两个问题都不是数据库性能问题而是“环境适配”问题。割接阶段的排错大部分精力其实花在这类“看着小、但会致命”的细节上。5.3 试运行期的监控指标和踩坑修复试运行持续了一个月我们按两周一次的节奏做复盘核心监控指标如下TPS和响应时间对比旧系统基线不能有明显退化每天检查慢日志阈值设为1秒超过就分析执行计划磁盘空间和增长速率估算扩容时间点每两周做一次主备切换演练确认自动切换可用至少做一次备份恢复演练确保RTO在四小时内。试运行期间发现了两条慢SQL都和统计信息过期有关。一条是库存汇总报表每天数据量都在涨但统计信息没有及时刷新优化器选了错误执行计划。修复方式是强制刷新统计信息并给核心报表SQL建议固化执行计划。另一条是客户主数据查询因为应用传入的变量类型和列类型不一致触发了隐式转换索引失效。修改JDBC参数的绑定类型后恢复正常。试运行阶段还要专门让业务用户提“以前不这样”的反馈。当时收集了20多条绝大部分是显示格式、小数位、日期格式、排序顺序的问题比如某质检报表的“合格率”显示成0。查下来发现源库字段用VARCHAR2存储“0.985”目标库在比较时按字符串处理逻辑判断全错了。这类问题要在老库阶段就清理干净脏数据新库再设置合适的列类型不能拖到试运行才处理。试运行期还有一个小技巧每天导出一份新库的日志和业务反馈和源库时期的历史同期做对比。这样能尽早发现“换了库后业务行为的细微变化”而不是等到用户忍不了才上报。这套对比机制很多团队不做我强烈建议做。6. 项目复盘几个值得同行借鉴的判断6.1 哪些环节被低估哪些被高估项目结束后我们做了一次完整复盘结论比较清醒。被低估的环节有三个。一是存储过程和包的改造量。一开始以为兼容度90%就代表只改10%实际比例大约是15%需要逻辑重构而且这15%集中了最多排错时间。二是数据校验的工作量。几千万行的大表校验脚本跑得慢需要提前优化并行度否则会卡住割接窗口。三是字符集相关的杂事。“看着能用”和“和原来一模一样”之间隔了无数个日期格式、排序规则、字符串截断问题。被高估的环节也有两个。一是对数据库引擎本身性能的担忧。实际跑下来日常OLTP吞吐完全够用国产库的短板已经不在引擎层。二是对迁移工具的期望。自动工具能解决70%的DDL和近100%的数据搬迁但复杂存储过程、函数索引、特殊对象还得靠手工。画一个简单的判断表环节预判实际建议投入表DDL转换工作量中等工具能搞定低存储过程改造主要靠工具30%需要人工介入高数据一致性校验简单COUNT要三层校验才放心高字符集与排序小事隐性bug集中地中压测与调优原库参数可复用每条核心SQL都要重看高6.2 给后来者的三个具体建议第一个建议迁移前先做业务血缘分析。把核心报表和批处理SQL涉及的表依赖关系理清楚哪些表是基础档案、哪些是流水、哪些是汇总结果各自有哪些上下游写入路径。我们一开始偷懒没做全后来排查数据校验不一致时经常要临时写SQL去查依赖浪费了不少时间。这个工作不复杂但要在迁移启动前完成。第二个建议不要把“兼容性好”理解成“零改造”。最稳的思路是让应用开发团队提前进入“方言隔离”状态把所有数据库特殊函数收敛到数据访问层不要在业务代码里到处写Oracle特性语法。这样即使后续再换库应用层不用大动。第三个建议原厂支持一定要买驻场不要只买线上远程。迁移项目里很多问题是现场才能快速定位的比如压测时等待事件分析、割接晚上的异常排查、试运行期间的参数微调。线上工单来回可能就要半天项目窗口耽误不起。买原厂DBA驻场花在人天上的钱比项目延期的成本便宜得多。最后再分享一个我的个人体会。做这种数据库国产化项目技术上真正难的往往不是数据库本身而是把旧习惯改掉。很多开发在源库上写了十多年的SQL换到新库就像换了一个语言环境一开始到处不习惯。但这种不习惯不是坏事它逼着我们把业务SQL写得更标准把执行计划看得更勤把每条核心查询的数据分布摸得更清楚。项目结束的时候团队里几个DBA反而觉得比原来更清楚这套ERP里的核心SQL到底长什么样、依赖哪些表、哪些索引最关键。这个改变可能比数据库本身更有价值。