去年下半年我接手了一个不太讨喜的活:把一套跑了七八年的 Oracle 11g 业务系统整体搬到 KingbaseES 上。系统不大,二十多个业务用户、四百多张表、几十个存储过程,但涉及资金流水和报表,数据一致性容不得半点马虎。整个过程踩的坑比我预想的多得多——数据类型看着一样,导进去就报错;存储过程语法过了,跑起来结果不对;应用端 SQL 在 Oracle 上跑得好好的,换库之后分页全乱。这篇就把 Oracle 数据库迁移至 KingbaseES 的完整链路拆开讲,从兼容性评估、环境初始化、结构与数据搬迁,到 PL/SQL 改写、应用层适配、性能验证和割接回滚,全部基于实际项目里的做法。适合正在做国产化替换的 DBA、后端开发和实施同学,也适合只是想了解两种数据库差异的人先建立一个整体认知。
1. 迁移之前先想清楚:哪些能平替,哪些必须重写
很多人接到迁移任务,第一反应是找一个工具,点几下把数据导过去就完事了。我一开始也这么想,结果第一次试迁就发现工具报了一百多条不兼容错误。问题在于 Oracle 和 KingbaseES 不是"同一个东西换了层皮",它是两套内核不同的数据库,只是 KingbaseES 提供了 Oracle 兼容模式来降低改造成本。兼容是"尽量兼容",不是"完全一样",这个边界在动手前必须先摸清楚,否则后面全是返工。
1.1 对象清单盘点与兼容性分级
正式动手前,我会先在 Oracle 侧把所有对象拉一份清单,按类型分类统计,然后逐类打兼容性标签。这一步看起来啰嗦,但它直接决定了后面工作量的估算准不准。
-- 统计各类对象数量 SELECT object_type, COUNT(*) FROM dba_objects WHERE owner IN ('APP_USER','REPORT_USER') AND object_type IN ('TABLE','INDEX','VIEW','SEQUENCE','PROCEDURE','FUNCTION', 'PACKAGE','PACKAGE BODY','TRIGGER','SYNONYM','TYPE') GROUP BY object_type ORDER BY 2 DESC;拿到清单之后,我一般按四档分类:
- 绿色(直接迁移):普通表、主键约束、普通 B-tree 索引、独立序列、视图。这类对象工具能自动处理,基本不用人工干预。
- 黄色(需参数调整):带分区的表、物化视图、位图索引、函数索引、外键依赖顺序复杂的表。需要手工确认迁移顺序和参数。
- 橙色(需语法改写):存储过程、函数、触发器、包。语法层面兼容模式能覆盖大一部分,但涉及 Oracle 特有包和动态 SQL 的地方必须逐个过。
- 红色(必须替换或废弃):DBMS_SCHEDULER 作业、DBMS_ALERT、高级复制、外部表、Oracle 特有的数据泵逻辑,以及用到了 Oracle 私有特性的自定义类型。
分级做完,我心里对"这个项目要多少人天"就有了大概的数。经验值是:纯表结构迁移占两成工作量,数据搬迁和校验占三成,PL/SQL 改写占三成,应用适配和联调占两成。如果你的系统存储过程特别多,第四块的比例会迅速爬升,这时候要提前跟项目组要人,别等到改到一半发现人力不够。
1.2 KingbaseES 的 Oracle 兼容模式到底覆盖了什么
KingbaseES 在初始化实例时可以指定兼容模式,Oracle 兼容模式是我们做迁移时的默认选择。这个模式打开之后,会带来一批"看起来跟 Oracle 一样"的能力,理解它是为了知道哪些地方可以偷懒、哪些地方偷不了。
覆盖得比较好的部分:ROWNUM伪列、DUAL虚拟表、NVL、DECODE、SYSDATE、TO_DATE/TO_CHAR的常用格式串、CONNECT BY层次查询、(+)形式的外连接、nextval/currval序列调用方式、部分DBMS_OUTPUT、DBMS_LOB内置包。像dual这种单行单列的虚拟表,兼容模式里也有同名的替代,写SELECT 1 FROM dual不会报错,只是它本身不存数据,不用担心"能存多大"这种问题,它永远只有一行。
覆盖得不完整、需要当心的部分:MERGE INTO的复杂用法、MODEL子句、PIVOT/UNPIVOT的部分边界场景、DBMS_SCHEDULER、UTL_FILE的目录对象、ROWID的语义、SELECT ... FOR UPDATE SKIP LOCKED的某些组合用法。还有一个极易被忽略的点:Oracle 的DATE类型是带时分秒的,而很多数据库的date只到天。KingbaseES 里对应带时间语义的应该用timestamp,如果工具按名字对齐把DATE映射成了date,时间部分会被直接截断,而这个过程往往不报错,等你发现报表日期全变成 00:00:00 的时候,数据已经迁完一轮了。
提示:在兼容模式选择上不要图省事。如果目标库实例是 PG 兼容模式创建的,后面再想用 Oracle 语法就只能靠改 SQL,代价非常大。实例初始化时就把模式定死。
2. 环境搭建与连接方式:从监听器到 ksql
环境这块,Oracle 老手最容易卡在"找不到对应物"上:Oracle 有监听器、有 tnsnames.ora、用 sqlplus 连;KingbaseES 的对应物是什么、端口是多少、客户端怎么连,这些如果提前理清楚,能省下不少瞎折腾的时间。
2.1 安装与初始化实例的关键参数
KingbaseES 的安装包一般由厂商提供,包含安装介质和授权文件(license)。授权文件是有有效期的,安装前先确认授权覆盖的目标环境、节点数和期限,别装完了发现授权不对还得重来。安装过程是图形化向导,中间会让你设置数据库超级用户的密码,这个密码就是后面 ksql 登录用的凭据,务必记牢——它不像某些系统有公开的默认初始密码,而是安装时你自己设定的。安装结束后要立刻确认服务是否起来:
# 查看服务状态 systemctl status kingbase8d # 命令行方式初始化一个 Oracle 兼容模式实例 # -D 数据目录 -m 兼容模式 -U 超级用户名 -W 交互输入密码 initdb -D /opt/Kingbase/ES/V8/data -m oracle -U system -W # 启动实例 sys_ctl -D /opt/Kingbase/ES/V8/data start初始化之后有几个参数一定要检查,它们直接影响后面的迁移和运行表现:
| 参数 | 建议值 | 说明 |
|---|---|---|
port | 54321(默认) | 换成 1521 也可以,但要和应用侧同步改 |
database_mode | oracle | 兼容模式,实例级设定,改不了 |
max_connections | 按业务峰值 × 1.5 | Oracle 的 process 概念在这里不适用 |
shared_buffers | 物理内存的 25%~40% | 参考值,需结合压测调 |
work_mem | 从 4MB 起调 | 排序和哈希连接吃这个 |
listen_addresses | 按实际网卡填 | 别用*图省事 |
password_encryption | 按安全要求选 | 影响新建用户的密码存储方式 |
max_connections这个参数特别值得说一句。Oracle 里你调processes和sessions,KingbaseES 这边每个连接是一个独立进程,连接数开太大内存会直接被吃穿。我见过有项目照搬 Oracle 的 3000 连接配置,结果库刚起来就被 OOM 干掉了。正确做法是先看应用连接池的最大连接数,再留出管理连接和后台任务的余量,一般几百就够了。
2.2 客户端工具链的替换对照
开发同学最关心的是"我平时用的工具还能不能用"。这块的对应关系大致是这样:
- sqlplus → ksql:命令行交互式客户端,功能定位一致。连接命令是
ksql -U system -d test -h 127.0.0.1 -p 54321,进到里面之后\d看表结构、\l看库列表,跟 sqlplus 的desc、select是不同套路,需要适应几天。 - PL/SQL Developer → 支持 KingbaseES 的图形客户端:目前主流的第三方数据库客户端都能连,需要在驱动管理里加载 KingbaseES 的 JDBC 驱动,然后按 JDBC 方式建连接,填主机、端口、库名、用户名密码即可。
- tnsping → 直接用 ksql 试连:没有单独的连通性测试工具,用 ksql 连一下最快。
- 数据泵 expdp/impdp → KDTS 或 sys_dump/ksql 的 COPY:小数据量用文本导出导入,大数据量上迁移工具。
这里有个小坑:Oracle 里dba_开头的视图、v$开头的动态性能视图,在 KingbaseES 里对应的是sys_前缀或者不同名字的系统视图。写运维脚本、监控脚本的时候这些都要重写,不能指望原样照搬。我一般会先整理一份"我常用的系统视图对照表",把dba_tables、dba_indexes、v$session、v$sql这些高频视图的新名字查清楚贴在文档里,团队里谁要用直接查表,比每次都去翻手册效率高。
3. 结构与数据迁移:工具能干的活和干不了的活
到了真正搬数据这一步,工具确实能省很多事,但如果你指望它全自动搞定,一定会被现实教育。我的经验是:结构迁移让工具打底,人工过一遍;数据迁移分成批导和校验两个独立阶段做。这两件事混在一起做,出了问题根本分不清是结构不对还是数据没进去。
3.1 用 KDTS 做全量结构与数据迁移
KDTS 是常用的图形化迁移工具,支持从 Oracle 到 KingbaseES 的对象和数据迁移。典型流程是:新建迁移任务 → 配置源库(Oracle 的 IP、端口、服务名、账号)→ 配置目标库(KingbaseES 的连接信息)→ 选择要迁移的模式和对象 → 执行。
配置源库这一步要特别注意账号权限。用普通业务账号连过去,工具读不到完整的对象定义,视图和存储过程很可能迁不过来。建议给迁移账号授予只读的 DBA 权限,或者至少要有SELECT ANY DICTIONARY、SELECT ANY TABLE这类权限。
执行之前,工具一般会先做一次"兼容性检查",把识别出来的不兼容对象列出来。这个报告非常关键,它是你后面改写工作的任务清单。我习惯把这个报告导出来,按对象类型排序,然后一条一条标记处理状态,改完一条划掉一条,比凭记忆干活靠谱得多。
迁移顺序上,工具通常会自动处理依赖关系(先建表、再建索引、后建约束和触发器)。但我遇到过分区表和外键交织的场景,工具生成的顺序仍然会报错,这时候就得手工把外键约束脚本抽出来,等所有表和数据都到位之后再单独执行。
3.2 表结构迁移中高频出问题的字段类型
类型映射是结构迁移里最容易出问题的地方,我整理了一份实际项目里最常踩的类型对照:
| Oracle 类型 | 常见错误映射 | 推荐映射 | 说明 |
|---|---|---|---|
DATE | date | timestamp | Oracle DATE 含时分秒,映射成 date 会丢时间 |
NUMBER(p,s) | 整数类型 | numeric(p,s) | 直接映射成 int 会把小数位抹掉 |
NUMBER(无精度) | int | numeric | 无精度 NUMBER 可能是任意大小 |
VARCHAR2(n) | 字节/字符混淆 | varchar(n) | 注意长度语义是字节还是字符 |
CLOB | clob型 | text | 大文本直接落 text |
BLOB | 二进制错位 | bytea | 二进制大对象 |
RAW(n) | 字符型 | bytea | 原始字节 |
LONG | 不支持 | text | 老系统遗留类型,必须先转换 |
TIMESTAMP WITH TIME ZONE | 丢时区 | 带时区的时间类型 | 跨时区业务必须保留 |
NUMBER的映射我单独说一下。Oracle 里NUMBER(10,2)表示总共 10 位、其中 2 位小数,金额字段大量用它。如果工具把它识别成整数,迁移时不报错,但所有金额的小数部分会被截断——这种错误在上线前如果没校验出来,后果非常严重。迁移完一定要抽样比对各表金额字段的小数位。
另一个高频问题是字符串长度。Oracle 的VARCHAR2(50)默认按字节算,一个中文占 3 个字节(UTF-8 下),所以实际只能放 16 个左右汉字;但很多数据库按字符算,varchar(50)能放 50 个汉字。听起来是"变得更宽松了",但如果目标库没按字符语义建,同时应用侧又按字节截断写入,就可能出现一边能存一边存不下的错位。稳妥做法是:迁移前统一确认两边的长度语义,把应用里所有写库前的长度校验逻辑跟着改一遍。
3.3 数据校验:行数对了不代表数据对了
数据校验这块,很多人的做法是比一下行数,对上了就收工。我只能说,行数相等但数据错位的情况我遇到过不止一次。真正能兜住的是三层校验:
第一层,行数比对。每张表 Oracle 侧和 KingbaseES 侧各COUNT(*)一次,逐表比对。这一层只能筛出"数据没导全"这种粗问题。
第二层,主键哈希比对。对所有主键值做一次聚合哈希,两边对比。这个能发现"行数一样但主键内容有差异"的情况,比如某几行丢了、另外几行重复了。
-- Oracle 侧 SELECT SUM(ORA_HASH(TO_CHAR(id))) FROM t_order; -- KingbaseES 侧,用 md5 或 hash 聚合 SELECT SUM(('x' || md5(id::text))::bit(32)::bigint) FROM t_order;第三层,业务汇总比对。选几个有业务含义的数值字段(金额、数量、积分),两边求SUM和AVG,差异率超过 0.01% 就报警。这一层是最有说服力的,因为它直接对应业务方关心的数字。
注意:哈希校验要用稳定一致的算法,两边算出来的值才能比。如果表里有浮点字段,哈希结果对精度极其敏感,建议先把浮点转成定点字符串再算,否则你会被莫名其妙的不一致折磨很久。
分批导大数据量的时候,别用单线程一次导完。按主键区间切成若干批,比如每次 50 万行,一批导完立刻校验一批,有问题当场定位。全量导完再校验,出问题就得从头排查,效率差十倍。切割批次时注意不要按ROWNUM切,因为ROWNUM每次查询都会重新编号,应该按主键范围切:
SELECT * FROM t_order WHERE id BETWEEN 1000001 AND 1500000;4. PL/SQL 改写:存储过程、函数与包的移植
存储过程这块是迁移里最耗时的部分,也是最容易埋雷的部分。语法能过不代表逻辑对,逻辑对了不代表性能可接受。我处理过的项目里,一个三百行的存储过程,语法改写只花了两小时,但把结果跑到和 Oracle 完全一致,花了两天。
4.1 语法差异清单与改写策略
先说大原则:能靠兼容模式过的就用兼容模式,过不了的再改语法,改语法时优先改成标准 SQL 风格而不是换个写法继续绑死某个库。理由很简单,标准写法以后换库、升级都省事,而依赖兼容特性的写法虽然当下跑得通,但心里没底。
具体的差异清单,按出现频率排序:
NVL/NVL2/DECODE:兼容模式基本支持,但DECODE在复杂嵌套时可读性极差,我建议顺手改成CASE WHEN,逻辑更清楚,也更容易排查。SYSDATE/TRUNC(SYSDATE):兼容模式支持SYSDATE。TRUNC(SYSDATE)取当天零点同样可用,但更标准的写法是CURRENT_DATE或者对时间戳做date_trunc。TO_CHAR的毫秒格式:Oracle 里写TO_CHAR(sysdate,'YYYY-MM-DD HH24:MI:SS.FF3')取毫秒,换库之后格式串里的FF3可能不认,需要用目标库的毫秒占位符,或者在应用层格式化。这个点特别隐蔽,因为格式串不识别时往往不报错,只是返回一个奇怪的结果。TO_NUMBER遇上脏数据:Oracle 里TO_NUMBER('abc')直接抛异常。有些系统为了容错,会写一堆CASE WHEN REGEXP_LIKE(...)判断。迁移时如果目标库的隐式转换规则更宽松(比如空串当成 0),同一段逻辑结果就变了。我习惯把所有隐式转换显式化,宁可多写几行也别依赖默认行为。MERGE INTO:简单的WHEN MATCHED THEN UPDATE / WHEN NOT MATCHED THEN INSERT一般能过,但带DELETE分支或者多表关联的复杂写法要重点验证。- 动态 SQL:
EXECUTE IMMEDIATE基本对应,但拼接进去的ROWNUM、dual、Oracle 特有函数在运行时才暴露问题,静态检查根本看不出来。这类代码我建议全部打上标记,运行时挨个跑一遍。
4.2 游标、异常、动态SQL的对应写法
游标的写法差异不大,CURSOR ... IS SELECT ...的声明方式、OPEN/FETCH/CLOSE的流程基本一致。要注意的是%ROWTYPE和%TYPE的引用方式,在包体里跨对象引用类型定义时可能出现解析顺序问题,遇到报错就把类型定义提到最前面。
异常的对应关系需要重点过一遍:
| Oracle 异常 | 说明 | 迁移建议 |
|---|---|---|
NO_DATA_FOUND | 查询无结果 | 通常直接支持,验证一下 |
TOO_MANY_ROWS | 返回多行 | 直接支持 |
DUP_VAL_ON_INDEX | 唯一键冲突 | 直接支持 |
ZERO_DIVIDE | 除零 | 直接支持 |
VALUE_ERROR | 转换错误 | 语义可能不同,重点验证 |
OTHERS | 兜底 | 直接支持,但要把错误码和错误信息一并落日志 |
OTHERS分支我有个习惯:改写时一定要把SQLCODE和SQLERRM打出来,而且要在里面加行号信息。Oracle 里报错定位到行相对容易,换库之后如果没有行号,一个几百行的存储过程出错了你只能靠猜。我一般会在关键节点插RAISE NOTICE或者往日志表里写进度,把执行路径串起来。
动态 SQL 部分要特别注意参数绑定。Oracle 里EXECUTE IMMEDIATE sql_str USING p1, p2是标准写法,改写到目标库时如果用字符串拼接代替绑定变量,不仅性能差,还有注入风险。我见过改写时图省事直接拼字符串,结果运行时因为单引号转义问题报了一堆语法错误。
4.3 一个真实存储过程改写案例
举个我改过的例子。原逻辑是:查询某客户当天未结算订单,按金额降序取前 10 条,把订单号拼成一个逗号串返回,顺便把总额算出来。Oracle 版本大概是这样的:
CREATE OR REPLACE PROCEDURE p_get_top_orders( p_cust_id IN VARCHAR2, p_result OUT VARCHAR2, p_total OUT NUMBER ) AS v_ids VARCHAR2(4000); BEGIN SELECT LISTAGG(order_no, ',') WITHIN GROUP (ORDER BY amount DESC), SUM(amount) INTO v_ids, p_total FROM (SELECT order_no, amount FROM t_order WHERE cust_id = p_cust_id AND status = 'UNSETTLED' AND TRUNC(create_time) = TRUNC(SYSDATE) ORDER BY amount DESC) WHERE ROWNUM <= 10; p_result := NVL(v_ids, 'EMPTY'); EXCEPTION WHEN OTHERS THEN p_result := 'ERROR:' || SQLERRM; p_total := 0; END;这段代码里有四个迁移关注点:LISTAGG聚合、ROWNUM限行、TRUNC(SYSDATE)日期截断、NVL空值处理。改写后的版本我倾向于这样:
CREATE OR REPLACE PROCEDURE p_get_top_orders( p_cust_id IN VARCHAR, p_result OUT VARCHAR, p_total OUT NUMERIC ) AS v_ids VARCHAR(4000); BEGIN SELECT string_agg(order_no, ',' ORDER BY amount DESC), SUM(amount) INTO v_ids, p_total FROM (SELECT order_no, amount FROM t_order WHERE cust_id = p_cust_id AND status = 'UNSETTLED' AND create_time >= CURRENT_DATE AND create_time < CURRENT_DATE + INTERVAL '1 day' ORDER BY amount DESC LIMIT 10) t; p_result := COALESCE(v_ids, 'EMPTY'); EXCEPTION WHEN OTHERS THEN p_result := 'ERROR:' || SQLERRM; p_total := 0; END;改写时我把三个地方做了替换。第一,LISTAGG换成了目标库的字符串聚合函数,排序放在聚合函数内部。第二,ROWNUM限行换成了LIMIT,注意这里必须放在子查询里面,写在最外层会把SUM的语义改掉——SUM必须对全部未结算订单求和,而不是只对前 10 条求和,这个细节如果改错,返回的总额就完全不对了。第三,TRUNC(create_time) = TRUNC(SYSDATE)改成范围比较>= CURRENT_DATE AND < CURRENT_DATE + 1 day,这样能走索引,比函数包裹字段高效得多。
这个案例我想说的是:改写的难点从来不是语法,而是想清楚每段代码的业务意图。ROWNUM <= 10到底是在限制聚合前的行还是聚合后的行,只有理解了业务才能改对。
5. 应用层适配:JDBC、分页、序列与dual
存储过程改完,剩下的战场就转到应用层了。这部分的特点是问题分散、影响面广,一个连接串写错就能让整个服务起不来,而修复只是改一个字符。所以我习惯列一张清单,把应用侧要动的地方全列出来,逐项打勾。
5.1 驱动与连接串的切换
最基础的三件事:换驱动包、改连接串、改方言配置。
驱动换成 KingbaseES 提供的 JDBC 驱动,驱动类名也要跟着换。连接串的格式大致是jdbc:kingbase8://主机:54321/库名,参数里注意字符编码设置,中文乱码十有八九出在这里。
如果用的是 ORM 框架,还要确认方言(Dialect)有没有对应支持。有些框架版本较老,没有内置目标库方言,需要用到兼容方言或者自定义方言类。这个点最容易在启动时才暴露,服务启动报"找不到方言"之类的错误,建议在测试环境先把服务起一遍。
连接池参数也要重新评估。Oracle 时代配的初始连接数、最大连接数、连接超时,换库之后要重新压测。前面提过每个连接是一个进程,这和 Oracle 的连接模型差别很大,连接池开得过大反而会拖垮数据库。
5.2 分页语句与ROWNUM的替换
分页是最典型的必须改写的 SQL 模式。Oracle 的经典三层嵌套分页:
SELECT * FROM ( SELECT a.*, ROWNUM rn FROM ( SELECT id, name FROM t_user ORDER BY create_time DESC ) a WHERE ROWNUM <= 20 ) WHERE rn > 10;换到目标库,直接写成:
SELECT id, name FROM t_user ORDER BY create_time DESC LIMIT 10 OFFSET 10;看起来简单,但有两个坑。第一,如果是 MyBatis 这类框架自动拼分页,可能不需要手工改 SQL,只要换方言。但如果代码里有手写的分页 SQL,就要全部搜一遍ROWNUM关键字,一个都别漏。第二,ORDER BY一定要有确定性的排序字段。Oracle 的三层嵌套分页在排序字段有重复值的时候,返回顺序本身就不稳定;换成LIMIT/OFFSET之后同样不稳定,但表现可能不同——测试时看着正常,上线后并发一上来,分页出现重复数据或者漏数据,这种问题极难排查。治本的做法是排序字段后面补一个唯一键,比如ORDER BY create_time DESC, id DESC。
提示:全库搜索
ROWNUM、dual、(+)、nextval这几个关键字,做一份替换清单,比一个一个模块点开找高效得多。
5.3 序列与自增主键的处理
序列这块相对平滑。seq_name.nextval和seq_name.currval在兼容模式下一般能直接用,但如果原本是SELECT seq_name.nextval FROM dual这种写法,建议改成SELECT nextval('seq_name'),减少一层对兼容特性的依赖。
触发器配合序列生成主键的经典模式,迁移后可以保留,但更推荐改成自增列或者默认值表达式,性能和可维护性都更好。如果保留触发器方案,务必检查触发器的执行时机(BEFORE INSERT还是AFTER INSERT)和NEW.id的赋值方式,这两处经常出问题。
还要注意序列的当前值要不要同步。迁移完数据之后,如果目标库的序列还停在初始值,应用一插入主键就冲突。正确做法是把序列值设置到当前表最大 ID 之上:
-- 查一下当前最大 id,然后把序列重置到它之后 SELECT setval('seq_order_id', (SELECT MAX(id) FROM t_order) + 1, false);这一步千万别忘,我见过上线当天因为序列没同步导致所有插入全部失败的情况,五分钟的修复花了两小时的排查。
5.4 常见 SQL 函数差异速查表
应用代码里的函数替换量最大,我整理了一份高频对照:
| 场景 | Oracle 写法 | 迁移后建议写法 |
|---|---|---|
| 空值替换 | NVL(a, 0) | COALESCE(a, 0) |
| 条件取值 | DECODE(a, 1, 'x', 'y') | CASE WHEN a = 1 THEN 'x' ELSE 'y' END |
| 当前时间 | SYSDATE | CURRENT_TIMESTAMP |
| 日期截断 | TRUNC(d) | d::date或date_trunc('day', d) |
| 字符串拼接 | a || b | a || b或concat(a,b) |
| 截取子串 | SUBSTR(s, 2, 3) | SUBSTR(s, 2, 3)或substring(s,2,3) |
| 查找位置 | INSTR(s, 'x') | POSITION('x' IN s)或strpos(s,'x') |
| 聚合拼接 | LISTAGG(x, ',') | string_agg(x, ',') |
| 取整 | CEIL/CEILING | CEIL |
| 四舍五入 | ROUND(n, 2) | ROUND(n::numeric, 2) |
这里我想强调ROUND和隐式类型转换的关系。Oracle 里ROUND('12.345', 2)能隐式把字符串转成数字再四舍五入,目标库对隐式转换更严格,可能直接报类型错误。遇到这类报错不要想着"加个转换函数糊过去",而要回到源头问一句:为什么这里是字符串?多半是上游字段类型设计的问题,借迁移的机会顺手修掉更划算。
还有一类特别容易被忽略的:TO_NUMBER转换遇到不可转的字符串。Oracle 里如果字段里混了非数字内容,TO_NUMBER会抛错。迁移后可以通过正则先判断再做转换,或者用CASE WHEN 字段 ~ '^[0-9.]+$' THEN 字段::numeric ELSE NULL END这种写法兜底。不要用全局替换去批量改这类 SQL,一定要逐个确认业务上"转不了数字"时应该返回什么值——是 NULL、0,还是跳过这行,这三种选择对报表的影响完全不同。
6. 性能对比、压测与回滚方案
数据和代码都迁完之后,最后一段路是性能和上线。我见过不少项目到这一步就松劲了,觉得"功能都通了,直接上吧",结果上线第一周被慢查询拖垮。迁移后的性能表现和 Oracle 时代一定不一样,有的语句变快,有的变慢,必须实测。
6.1 执行计划与索引重建
Oracle 上跑得快的语句,换库之后变慢,八成是执行计划变了。常见原因是统计信息不准、索引没建全、或者优化器对连接顺序的选择不同。
统计信息这块,迁移完数据之后必须手动收集一次全库统计信息。Oracle 有自动统计任务,目标库那边也有类似的机制,但刚迁完的时候统计信息是空的或者过期的,优化器等于在盲猜。收集完再跑一遍关键 SQL,很多"慢查询"会自己消失。
索引方面,工具迁移时通常能带上普通 B-tree 索引,但下面几类要单独确认:
- 函数索引:Oracle 的
CREATE INDEX ... ON t(TO_CHAR(d,'YYYYMMDD'))这类,迁移后需要确认表达式能正确重建。 - 位图索引:目标库可能不支持或者有性能差异,建议在迁移前就评估是否可以用普通索引替代。
- 分区索引:本地索引和全局索引的行为差异,要结合分区表的实际查询模式验证。
- 复合索引的列顺序:迁移不会改变列顺序,但优化器利用索引的方式可能不同,配合统计信息一起看。
验证方法很直接:挑出生产环境 Top 20 的慢 SQL 和目标 SQL,两边各跑一次执行计划,对比实际耗时。我在实际项目里会做一张对照表:
| SQL 标识 | Oracle 耗时 | 迁移后耗时 | 差异 | 处理措施 |
|---|---|---|---|---|
| 订单列表查询 | 120ms | 850ms | 变慢 | 补复合索引 |
| 月度汇总报表 | 4.2s | 1.8s | 变快 | 无需处理 |
| 客户模糊搜索 | 300ms | 300ms | 持平 | 无需处理 |
| 明细关联查询 | 1.1s | 6.5s | 严重变慢 | 改连接方式+加索引 |
差异超过 2 倍的都要查原因,不能上线。
6.2 割接窗口与回滚预案
真正的割接只占整个项目很小一段时间,但它决定了成败。我的做法是把割接分成三个阶段:预演、正式割接、观察期。
预演至少做两次,用生产数据的全量副本,把整条链路完整跑一遍:停应用、导数据、改配置、起应用、跑冒烟用例。预演的目的是把脚本里的手误和遗漏全部暴露出来。第二次预演一定要掐表,记录每个步骤的实际耗时,这样正式割接的时间窗口才估得准。
正式割接的时间点选在业务低谷,窗口长度按预演耗时的 1.5 倍准备。步骤大概是:业务侧停止写入 → 记录 Oracle 侧的当前状态(序列值、最大时间戳)→ 做增量数据同步 → 校验数据 → 切换应用连接串 → 启动应用 → 冒烟验证 → 观察。
回滚预案必须提前写好并且验证过。核心是两件事:Oracle 侧的库在观察期内不删、不改;应用配置的切换要能一键切回。我一般会把连接串配置做成独立配置文件,切换前后各备份一份,回滚时只需要替换文件并重启服务,避免手忙脚乱地改代码。
观察期至少留一周,重点盯三个指标:慢查询数量、连接池使用率、错误日志里的异常类型。新增的异常往往指向某段没改干净的 SQL 或者某个类型转换问题,早发现一天就少影响一批用户。
最后分享一个我在实际操作中的体会:迁移项目里最值钱的产出不是迁移脚本,而是那份"不兼容清单"和"改写记录"。前者告诉你哪些对象有风险,后者告诉你每个风险是怎么解决的。有了这两份文档,下次再迁另一套系统,你的效率至少翻一倍。我在处理第二个项目的时候,直接拿上一份改写记录当模板,存储过程部分的工作量压缩了将近六成。所以从第一天开始就把记录做起来,别嫌麻烦。