1. 为什么选金仓:替换 Oracle 的选型思路与兼容性底气
这几年数据库国产化已经不是一个概念了,而是落到每一个具体项目里的硬指标。我接手过不少 Oracle 替换的活儿,客户通常上来就问一句:能不能不改代码就迁过去?这话听着像异想天开,但金仓(KingbaseES)偏偏就是冲着这个需求去设计兼容性的。今天这篇我不讲空话,直接复盘一个从 Oracle 19c 迁移到金仓的完整工程,把选型、评估、迁移、上线、排障的全过程掰开揉碎,尤其是“零改造”这三个字到底怎么落地。
先说结论:金仓能做到“零改造”,不是因为它把 Oracle 的代码模仿了个形似,而是它在语法解析层、数据类型映射、PL/SQL 存储过程语义、常用系统包这几个维度上做了深度兼容。但这不意味着你可以拿着生产库直接跑,前期的兼容性摸底、对象迁移顺序、数据校验方案、切换演练,一样都不能省。我见过有人图省事,schema 一导,连接串一改就敢上线,结果 ROWNUM 分页查出来的数据对不上、存储过程里隐式游标行为不一致,生产直接一锅粥。
先说选型。Oracle 替换不是只有金仓一个选项,市面上还有基于 PostgreSQL 生态的、自研 OLTP 行列混合的、以及走 MySQL 兼容路线的,但我评估下来,金仓在 Oracle 兼容这个细分场景里确实有先天优势。它的内核虽然源自开源 PG,但在 Oracle 兼容模式上做了大量改造,不是简单挂一层语法翻译,而是从解析器到执行器都保留了 Oracle 的语义。最直观的例子:我迁完以后,项目里同事问我“是不是真没改 SQL?”,我说你翻翻 Git 提交记录,业务库的存储过程一个字节没动,就改了数据源配置。这才是真兼容。
当然,选型不是只看宣传页,我会让厂商提供一个评估镜像,然后自己写一套覆盖典型场景的压测脚本,包含 OLTP 事务、批量作业、存储过程递归调用、物化视图刷新、分区表 DML,跑完再决定。纸上谈兵没用,数据库这种基础设施选错了,后面擦屁股的成本比选型省下来的那点时间高一个量级。
1.1 兼容性验证:先跑通这五类典型场景再谈零改造
零改造不是玄学,它是一级一级验证出来的。我通常把验证集分成五类:基础 SQL(含连表、子查询、分组、排序)、常用函数(NVL、DECODE、TO_CHAR、TO_DATE、SYSDATE、TRUNC 这类高频函数)、PL/SQL 块(存储过程、函数、包、触发器)、特殊语法(CONNECT BY 层级查询、MERGE INTO、ROWNUM 分页、自治事务)、以及系统包(DBMS_OUTPUT、DBMS_LOCK、DBMS_JOB、UTL_FILE 这一类)。每一类都拿实际业务场景去验证,而不是跑几条 select 1 就算完。
举个例子,CONNECT BY 这东西,很多基于 PG 内核的数据库都不支持,但金仓在 Oracle 兼容模式里是直接可以跑的。我对着一张 60 万行的组织架构表递归查了 12 层,出来结果和 Oracle 完全一致,连 LEVEL 伪列都保留了。再比如 MERGE INTO,这个语法在 ETL 作业里用得非常多,金仓也原生支持,不需要改写成 INSERT + UPDATE 两条语句。这些点单个看都微不足道,但几百个这样的点叠加到一起,才是“零改造”的真正底气。
1.2 兼容开关:别忽略初始化参数和 db_mode
金仓的 Oracle 兼容不是默认全开的,它有几个关键开关。最基础的是数据库初始化时的兼容模式选择,一般有三种模式:Oracle 兼容模式、PostgreSQL 兼容模式、MySQL 兼容模式。如果你要用 Oracle 语法和 PL/SQL 特性,必须在 init 阶段就选择 Oracle 模式,而不是等初始化完再改。这个操作类似于你在建房子之前定框架,架子搭错了后面装修怎么补都别扭。
另外还有一批细粒度参数,比如空字符串与 NULL 的等价处理、日期类型的行为方式、双引号标识符的处理规则、字符串拼接时数字隐式转换的规则等。Oracle 里''和 NULL 是等价的,但 PG 内核里它们不等价,金仓通过兼容参数把它拉齐了。日期类型也一样,Oracle 的 DATE 类型包含时分秒,但 PG 的 DATE 只到天,如果不打开兼容参数,日期字段迁移过去以后查询结果就少了时间部分,报表数据对不上。这些参数必须在迁移之前就定好,因为初始化参数会影响列的存储格式,后面想改要动表结构,代价极大。
2. 迁移前盘家底:对象清单、依赖图谱与工作量预判
选型定了以后,千万不要急着导数据。我一般的做法是先花一到两周做现状梳理,把源库里所有对象摸一遍底。一套 Oracle 生产库,少说也有上千张表、几百个存储过程、几十个包和触发器,再加上序列、视图、物化视图、同义词、DBLINK、用户权限,挨个过一遍才知道工作量在哪里。这个过程我们内部叫“盘家底”。
盘家底的第一步是分层盘点对象类型。表、索引、约束这些结构对象相对好处理,规划好映射关系就行;但存储过程、函数、包、触发器这类代码对象才是真正的风险点,因为它们里面的写法可能用了一堆 Oracle 特有的函数和语法特性。另外别忘了还有序列,Oracle 的序列在很多系统里被用来生成主键,序列迁移如果没做好,最常见的结果是报表系统上线当天就撞主键,因为两者序列当前位置没对齐。
第二步是摸清对象依赖关系。视图依赖表、存储过程依赖视图、触发器依赖表,这种依赖链条一旦断掉,你在迁移目标库里面跑 CREATE OR REPLACE 视图会直接报“关系不存在”。我习惯先把对象依赖图谱导出来,用脚本把存储过程里面引用的所有表名和函数名抽出来,做成一张依赖矩阵,然后按照“无依赖对象 → 有依赖对象”的顺序去建。这样做的好处是减少返工,不然今天建了存储过程,明天发现依赖的表还没建,删了重建浪费时间。
2.1 数据类型映射表:NUMBER、VARCHAR2 这些都要逐一对应
数据类型映射是整个迁移最容易出暗坑的地方。Oracle 的 NUMBER 类型是一个变长数值类型,可以存整数也可以存小数,精度可以指定也可以不指定。金仓这边一般映射成 NUMERIC 或 DECIMAL,语义上是兼容的,但要注意 Oracle 里NUMBER不带精度时,金仓默认映射的参数不同,可能会影响高精度数值的存储。我建议迁移前做一次全库扫描,把所有字段长度超过 20 位的 NUMBER 列拉出来,逐个确认这些字段到底是数值型的主键还是业务数值,避免迁移后精度溢出。
VARCHAR2 映射到 VARCHAR,这个比较简单,但要注意字符集。Oracle 里如果用 AL32UTF8,那么 VARCHAR2 的长度单位是字节数上限,但金仓的 VARCHAR 长度单位是字符数,迁移 DDL 的时候要按实际字节数重新换算,否则中文字段会报长度超限。CLOB 映射成 TEXT 或 CLOB 都行,我建议在主键和索引约束不涉及的场景下直接映射 TEXT,处理起来更简单。DATE、TIMESTAMP 要确认是否打开了日期兼容开关,金仓的 DATE 在兼容模式下可以带时分秒,这是迁移前后报表结果一致性的关键前提。
2.2 代码对象改造评估:存过、函数、包到底有多少坑
代码对象是评估工作的重中之重,直接决定了“零改造”是口号还是现实。我最常用的方法就是批量扫描存储过程和包,找出所有 Oracle 特有的写法,然后逐条对照金仓兼容列表。常见的是 NVL、DECODE、SYSDATE、TO_CHAR、TO_DATE、TRUNC 这一票函数,金仓基本都支持,不用动;ROWNUM、ROW_NUM 分页、CONNECT BY、MERGE INTO 也支持,省心;比如CREATE OR REPLACE PACKAGE这种带包头的写法,金仓在 Oracle 兼容模式下也没问题。
真正需要留意的是那些依赖底层行为的写法,比如隐式游标属性(%FOUND、%ROWCOUNT)、自治事务(PRAGMA AUTONOMOUS_TRANSACTION)、以及使用了DBMS_LOCK这类高级系统包的业务逻辑。金仓的兼容模式里这些都有对应实现,但你要做的是把这类对象单独拉个清单,重点压测。我遇到过一次自治事务日志表在业务处理时不落数据的情况,排查了两天才发现是兼容参数没完全打开,所以在评估阶段给这些高级特性建立专项验证用例,能省下后面排障的好几天。
3. 零改造怎么落地:兼容开关、SQL 语法与 PL/SQL 关键差异
评估做完,接下来就是真正进入迁移实施阶段。先说一个很多人问的问题:零改造到底是字面意思还是打折的?我的回答是,字面上能实现,但你需要做三件事:第一,确认初始化兼容参数正确;第二,用兼容性扫描工具或自查脚本把所有不兼容写法提前清掉;第三,建立一个语法灰度验证环境,任何代码对象都是先验证后上线。这三件事做扎实了,你才敢拍着胸脯说零改造。
以曾经做过的一个 ERP 系统为例,Oracle 端有四百多个存储过程、六十多个包、两百多个触发器,代码量加起来超过三十万行。迁到金仓之后,我没有改存储过程内部的业务逻辑,只是在少数几个包里面调整了外部函数调用方式,原因是这些包调用了 Oracle 自带的UTL_FILE读写服务器文件系统,金仓虽然也有对应能力,但路径设置方式不同,这个属于环境配置差异而不是 SQL 不兼容。说实话,看到编译一次通过的时候,项目组所有人都松了口气。
3.1 分页查询与 ROWNUM 语义:最容易翻车的地方
如果你问我迁移过程中最容易被线上问题打脸的点是什么,我会毫不犹豫说是分页查询。Oracle 里很多老系统用的是ROWNUM <= N这种写法,比如SELECT * FROM (SELECT t.*, ROWNUM rn FROM table t) WHERE rn BETWEEN 1 AND 20。这种写法要求数据库在子查询里先给结果集编号,再在外面做范围过滤。金仓的 Oracle 兼容模式下,ROWNUM 伪列是保留的,所以这段 SQL 可以直接跑。但要特别注意,如果你用ROWNUM > 10这种过滤条件,Oracle 的行为是返回空集,因为 ROWNUM 是在结果集生成时递增赋值的,金仓为了兼容也保持了同样行为。这个“怪癖”如果团队里有从 MySQL 转过来的开发,很容易踩坑,他会觉得金仓和 MySQL 的LIMIT 10 OFFSET 20行为应该一样,实际上完全不是一回事。
另一种分页写法是FETCH FIRST 20 ROWS ONLY,这种是 Oracle 12c 以后的新语法,金仓也支持,但前提是数据库版本和兼容参数要到位。我建议迁移前统一扫描一遍所有分页 SQL,把ROWNUM和FETCH FIRST两类写法都列出来,各抽几条典型语句做回归测试。最怕的是生产环境里混着两种写法,测试只覆盖了一种,上线后另一种出问题,那叫一个措手不及。
3.2 存储过程与触发器:从编译到执行的完整链路
存储过程是迁移的核心,因为业务逻辑都封在里面。金仓在 Oracle 兼容模式下支持CREATE OR REPLACE PROCEDURE、FUNCTION、PACKAGE、PACKAGE BODY以及各种类型的触发器。变量的%TYPE和%ROWTYPE声明方式、IF/ELSIF判断、LOOP循环、CURSOR游标、EXCEPTION WHEN OTHERS异常块,这些 PL/SQL 的常规写法在金仓里都能编译通过。
说一个细节,Oracle 存储过程里经常用到SELECT ... INTO把查询结果赋给变量,如果查询不到记录,会触发NO_DATA_FOUND异常。金仓在这块的行为和 Oracle 保持一致,所以代码里的异常捕获逻辑不用改。另一个细节是SQL%ROWCOUNT,这个属性在 DML 语句执行后返回影响行数,金仓也做了兼容,事务处理逻辑能原样搬。触发器部分要注意触发器的创建顺序,因为触发器体里可能引用了别的触发器或存储过程,建议创建顺序遵循依赖关系,否则会遇到依赖对象不存在导致创建失败,但这不是语法不兼容,只是流程编排问题。
3.3 特殊语法兼容速查:CONNECT BY、MERGE INTO、自治事务
把一部分特殊语法单独列出来是有原因的,这些语法在 OLTP 系统里可能用到的不多,但一旦用到,往往是核心业务逻辑。CONNECT BY 层级查询在处理组织架构、BOM 展开、科目树这类递归结构时是刚需。金仓的 Oracle 兼容模式下,CONNECT BY 不仅支持,还保留了LEVEL伪列和SYS_CONNECT_BY_PATH函数,所以以前怎么写现在还是怎么写。MERGE INTO 做增量更新与插入的合并且非常常用,金仓也支持,我在做数据同步作业的时候就用它替代了“先 DELETE 再 INSERT”的老方案,减少了表锁竞争和 UNDO 压力。
自治事务这块值得多说两句。很多业务系统里有用PRAGMA AUTONOMOUS_TRANSACTION做错误日志记录的场景,主事务回滚了,日志记录不能被回滚掉。金仓兼容了这个语法,但关键点是数据库要开启对应的兼容选项,同时日志表和普通业务表的事务隔离级别要配对。我遇到过一个案例,存储过程像往常一样调用自治事务记录日志,生产环境金仓日志表一条记录都没写,检查到最后发现是会话级参数覆盖了全局参数。我的经验是,迁移后 DBA 要重新确认连接池里每个连接的会话参数都继承全局配置,不能有自定义覆盖。
4. 数据搬迁与一致性校验:从全量导出到增量追平
结构对象迁移完成以后,接下来才是最耗时也最不能出错的部分——数据搬迁。这一步跟业务系统的高可用都有关,迁不好,前面所有工作白搭。我先说整体方案:一般来说我们会采用“全量导出导入 + 增量追平”的方式,过渡期间源库不停机,利用日志或业务低峰期窗口做一次最终同步,再整体切流量。Oracle 到金仓的数据迁移工具比较多,我常用的是两边都是 SQL 类数据库,直接用专业数据迁移工具或自定义 ETL 脚本都能搞定。
有一个事项必须提前做好:数据迁移前要彻底关闭或跳过外键约束检查。如果按照默认顺序去导,主表没导入完成之前子表就开始插数,外键校验直接失败。所以执行顺序通常是:先关闭约束,导完数据后再重建约束并做全表一致性校验。这个环节我吃过一次亏,当时图省事没关外键,导到一半报错,清掉数据重来不说,源库和目标库数据还因为中间断点产生不一致,排查浪费了大半天。教训就一句话:别跟数据库的约束机制较劲,它拦你是怕脏数据,现在你比它更清楚你要干什么。
4.1 数据校验方案:行数、主键、校验和三层验证
做完数据导入,最后一公里就是校验。只比行数远远不够,行数一致不代表数据一致,我以前用过一个三层校验法,效果还不错。第一层是行数和主键范围比对,通过每个表的主键最大值、最小值、行数快速比对,能确认大体数量级没问题。第二层是抽样字段校验,对每张表随机抽几条记录,比较关键业务字段,比如金额、日期、状态字段,看有没有因类型转换或精度问题导致的值偏差。第三层是全字段哈希校验,对两边的表做 MD5 或自定义哈希聚合,比对结果,这一层最严但耗时也最长。
实践里有个坑需要提醒:Oracle 里 DATE 类型的内部存储格式和金仓不太一样,直接做文本级比对会误报不一致。我把日期字段统一转成格式化的字符串再参与哈希,这样两侧的比对才公平。金额字段也要注意浮点精度问题,最好转成字符串或定点数再哈希。校验脚本写完之后先跑一个小库验证正确性,没问题之后再全库跑,别一上来就全量执行,不然造出来的误报能淹死你。
4.2 序列与自增值的处理:避免上线撞主键
数据迁移的另一个细节是序列起始值。Oracle 的序列当前值会记录在字典里,但导出工具默认不会自动同步到金仓的序列定义中。如果直接建一个初始值为 1 的序列,而上线时业务表里已经有十万条数据,那么系统一插入新记录就报主键冲突。处理办法很简单:迁移完数据以后,对每个序列做一次setval,把起始值设置为源库序列当前值加上一个安全余量。
我一般会把余量加大到原值的 120%,比如源库序列当前值是 10000,目标库就设成 12000,这样就算迁移期间源库又有少量数据写入,也不会出现两边重叠的问题。不要小看这个余量,我曾经在双写演练场景里因为余量不够,目标库序列被追平,两个系统同时生成的单号撞了,虽然业务上没有造成损失,但这个问题暴露出来的时候还是让人出了一身汗。序列这块就一句话总结:提前对账,打足余量。
4.3 增量追平:从源库到目标库的最后一段同步
如果业务系统不允许长时间停机,那就需要增量同步方案。方法分两种:一是数据迁移工具的增量同步功能,它会读取源库日志并解析成对目标库的改写操作;二是通过时间戳增量查询,在业务表里找UPDATE_TIME或CREATE_TIME字段,定期把修改过的记录同步过来。
时间戳方案实现简单,但依赖业务表必须有更新时间的字段,而且只能捕获数据变化,无法捕获结构变化。日志解析方案功能强,但部署复杂,对数据库性能和日志保留期有额外要求。我建议根据业务复杂度来选,核心交易类表用日志解析同步,普通配置表用时间戳增量就够。增量同步阶段要留意延迟指标,一般控制在秒级以内才能安全切换,如果延迟常年超过分钟级,说明同步目标集群性能不够,要多开并行通道。
5. 上线切换与坑点实录:排障手记和速查表
上线切换的时刻,是所有前期准备的试金石。切换方案里最忌讳的是“一次性大爆炸”式切换,就是某个固定时间点把连接串一改,所有流量直接打到新库。这种模式万一出了兼容性问题,回滚成本非常高。我习惯用灰度加双跑的策略:先切一个只读模块到金仓,跑报表查询、历史数据查询;没问题之后,切一个非核心的写模块;最后再把核心交易模块切过去。每一阶段都有独立的回滚方案,一旦出问题就把连接切回 Oracle,不影响业务。
这里又牵扯出一个常见问题:应用要不要改代码?如果用的是 JDBC 标准接口和标准 SQL 做简单操作,那应用基本不用动。就怕有人在 SQL 里写了 Oracle 特定的函数而不自知,比如TO_DATE(‘2024-01-01’, ‘YYYY-MM-DD’)没问题,但如果是TO_CHAR(SYSDATE, ‘DD-MON-YY’)这种依赖 NLS 参数的格式化写法,两边的默认设置不一样,结果可能不同。所以我给出的建议是:应用侧改数据源配置,但不要让应用代码里使用多数据库方言,把 SQL 标准化这件事,放在迁移之前做。
5.1 常见问题速查表:20 个高频坑一次理清
| 问题现象 | 可能原因 | 处理手段 |
|---|---|---|
| 日期查询少了时分秒 | DATE 兼容参数未开启 | 初始化阶段打开日期兼容选项 |
| 空字符串与 NULL 行为异常 | 空字符串兼容参数未设置 | 打开空字符串等价 NULL 开关 |
| 中文排序不对 | 排序规则字符集不同 | 统一数据库字符集为 UTF-8,必要时指定排序规则 |
| CLOB 字段写入报错 | 长文本超出字段上限 | 扩展目标字段或改为分段写入 |
| 分页查询结果重复 | ROWNUM 与 ORDER BY 执行顺序不同 | 检查 SQL 写法,先排序再编号 |
| 存储过程编译失败 | 依赖对象还没创建 | 按对象依赖顺序重新创建 |
| 自治事务日志不落库 | 会话参数覆盖了全局参数 | 查看连接池会话级参数并调整 |
| 大事务回滚时间过长 | UNDO 表空间或等价资源不足 | 调大回滚段空间,分批提交事务 |
| 序列撞主键 | 序列起始值未同步 | setval 到源库当前值加余量 |
| 视图查询报列不存在 | 依赖表字段顺序不一致 | 重新生成视图并比对元数据 |
| DBLINK 无法使用 | 目标库没有等价外部数据源特性 | 改成应用层跨库调用或数据同步 |
| 触发器未按预期执行 | 触发时机配置差异 | 对比触发器定义,确认 BEFORE/AFTER 级别 |
| 复合索引失效 | 类型隐式转换导致列无法走索引 | 修正字段类型或 SQL 写法 |
| 递归查询结果不一致 | 层级数据有环状引用 | 检查数据环并设置 CONNECT BY 环路处理 |
| 批量 INSERT 性能骤降 | 目标库参数未调优 | 调整批处理参数、关闭约束后批量导入 |
| 数值精度溢出错报 | NUMBER 映射精度不足 | 扫描超长 NUMBER 列单独处理 |
| 更新操作影响行数与预期不符 | 隐式类型转换匹配了不同记录 | 检查 WHERE 条件字段类型 |
| 存储过程死锁频发 | 锁粒度或隔离级别差异 | 调整事务隔离级别,优化锁顺序 |
| 物化视图刷新失败 | 刷新模式不兼容 | 改用手工刷新或定时任务刷新 |
| 监控告警连接数过高 | 连接池配置沿用旧库参数 | 按金仓默认并发规格重新配置池大小 |
这张表算是我个人踩坑经验的浓缩版,实际遇到任何一条,不要慌,先定位是参数问题、语法问题还是数据问题。参数问题优先排查兼容开关,语法问题用兼容模式再编译一遍,数据问题直接用三层校验法切分定位。
5.2 回滚预案与双写演练:上线前必须做的一件事
切换方案再完美,不演练也是纸上谈兵。我强烈建议上线前至少做两次完整的切换演练:第一次是功能验证,模拟日常交易和批量跑批,确保所有功能点都能跑通;第二次是故障演练,故意在切换途中制造故障,比如把目标库停掉、模拟连接超时,验证回滚方案是否真的能切回 Oracle 且数据不丢。
双写演练是另一个容易被低估的环节。在灰度切换期间,源库和目标库同时接收写入流,这时候要设计一套数据一致性核对机制,定期检查双写后两边的数据是否一致。我做过最好的方案是:在应用里加一个数据比对开关,同一笔写操作同时落到两个库,写入完成后立刻比对返回结果,不一致就记日志并报警。双写期间的比对结果直接决定正式切换的信心指数,如果双写跑了一周没有任何差异,大可以放心地把流量全部切过去,反之如果差异频出,那就得回过头排查兼容参数和映射规则。
5.3 从迁移到长期运维:上线后两周内的重点监控项
很多团队在系统切完以后就以为大功告成了,其实真正见真章的时刻是在上线后的头两周。这个阶段业务流量是真实流量,不经过任何演练过滤,很多只在生产环境下才会出现的边界问题都会浮出水面。我建议重点盯四类指标:慢查询数量、锁等待时间、数据库连接池使用率、以及错误日志里出现的 SQL 异常。任何一类指标出现趋势性上升,都要立即定位是哪条 SQL 或哪个存储过程的问题。
慢查询是重中之重。同样的 SQL,在 Oracle 里走索引,到了金仓可能因为统计信息还没收集导致执行计划变了。所以上线后第一件事就是全库跑一次统计信息更新,把迁移工具带来的旧统计信息刷掉,让优化器重新生成执行计划。我遇到过一条报表 SQL 在 Oracle 上跑 3 秒,迁移后第一次跑要 40 秒,执行计划一看是全表扫描。更新统计信息之后,加上了索引提示,耗时回到 2 秒以内。这种案例在迁移项目里比比皆是,不是数据库不行,是你没给优化器喂够信息。
连接池参数也是重灾区。Oracle 的连接数规格和金仓默认规格不一样,直接把旧的连接池配置搬过来,会出现初始化失败、连接长时间等待等问题。我给的建议是迁移后对照金仓默认配置,把最大连接数、最小空闲连接数、连接超时时间重新校准一遍,不要直接沿用 Oracle 时代的数值。这些琐碎的细节,才是决定系统长期稳不稳的关键变量。
个人一点经验:数据库替换不要把它当成一次性搬迁工程,而是一次长期的容量规划和性能调优过程。Oracle 里很多 DBA 的管理习惯到了金仓需要调整,比如表空间管理方式、统计信息更新频率、备份恢复演练节奏。把这些纳入运维体系,替换才算真正落地。至于那些号称“零改造”的方案,我的态度一直是不迷信、不排斥,拿测试数据说话,拿灰度结果说话,一步一步验证到生产环境无惊无险,这才是靠谱的工程态度。