说真的,接这个Oracle迁移到金仓项目的时候我心里是有点打鼓的。老系统跑了十几年,业务逻辑全绑在Oracle上,随便动一下都怕出问题。但项目推到这一步了,硬着头皮也得上。这几个星期折腾下来,踩了不少坑,也算摸出些门道。今天就随便聊聊这段经历,不是什么官方教程,就是些实际干活时的感受和遇到的坑。
兼容性这事儿,说起来容易做起来难
一开始看金仓的文档,Oracle兼容性这块说得天花乱坠的。什么SQL语法高度兼容、PL/SQL无缝迁移,看得人心里直犯嘀咕:真有这么神?
实际上手之后才发现,有些地方确实挺给力。比如简单的SQL语句,基本能直接跑:
-- 这种在Oracle里常见的SQL,在金仓里基本不用改
SELECT emp_id, emp_name, salary
FROM employees
WHERE dept_id = 10
ORDER BY salary DESC;
表结构迁移的时候,大部分常用类型都能自动映射。NUMBER转NUMBER,VARCHAR2转VARCHAR,DATE转DATE,这些基本不用操心。但是!但是!但是——重要的事情说三遍,有些细节很容易被忽略。
比如这个日期格式问题,我栽了个大跟头:
-- Oracle里的写法
INSERT INTO orders (order_date) VALUES ('11-10-10 10:10:10');
-- 结果金仓给解释成了 2010-11-10,但原来的意思是 2011-10-10
-- 这个坑真是踩得够呛,最后发现是配置问题
set ora_date_style = true;
还有这个字符类型长度的问题:
-- Oracle里CHAR(10)可能存10个字节或10个字符
-- 金仓里得明确是byte还是char
-- 不然后期查数据的时候发现多了不少空格
一开始我也没太在意这些细节,后来对账的时候发现数据对不上,才一个个找出来改。所以我的建议是:迁移的时候别想着一步到位,先把数据迁过来,然后仔细对账,把不一致的地方找出来慢慢改。
存储过程和函数,大部分能直接用
这块是我最担心的,毕竟老系统里有几千个存储过程和函数。结果发现金仓对PL/SQL的支持确实比我想象中要好不少。
比如这个简单的存储过程:
CREATE OR REPLACE PROCEDURE calculate_bonus (
p_emp_id IN NUMBER,
p_bonus OUT NUMBER
) AS
v_salary NUMBER;
v_score NUMBER;
BEGIN
SELECT salary, performance_score
INTO v_salary, v_score
FROM employees
WHERE emp_id = p_emp_id;
p_bonus := v_salary * v_score * 0.1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_bonus := 0;
END;
/
这个在金仓里直接就能跑,不用改。类似的,函数、触发器这些,大部分都能直接迁移。
不过也不是完全没问题。有些特殊的地方还是要处理:
-- Oracle里可以这样,包里同名同参数的函数和过程
CREATE OR REPLACE PACKAGE test_pkg AS
FUNCTION test_func (p_num NUMBER) RETURN NUMBER;
PROCEDURE test_proc (p_num NUMBER);
END test_pkg;
-- 金仓里这个不行,得改名字
CREATE OR REPLACE PACKAGE test_pkg AS
FUNCTION test_func (p_num NUMBER) RETURN NUMBER;
PROCEDURE test_proc_v2 (p_num NUMBER); -- 改个名字
END test_pkg;
还有这个自治事务的问题:
-- Oracle里的自治事务
CREATE OR REPLACE PROCEDURE audit_log AS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO audit_table (msg) VALUES ('test');
COMMIT;
END;
金仓也支持自治事务,但是语法可能需要调整。我记得当时改了好几个这样的存储过程,最后都跑通了。
游标这块,基本没遇到太大问题
游标的使用在老系统里很常见,迁移的时候基本不用改:
-- 显式游标,两个数据库都能跑
DECLARE
CURSOR emp_cursor IS
SELECT emp_id, emp_name, salary
FROM employees
WHERE dept_id = 10;
v_emp emp_cursor%ROWTYPE;
BEGIN
OPEN emp_cursor;
LOOP
FETCH emp_cursor INTO v_emp;
EXIT WHEN emp_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp.emp_name || ': ' || v_emp.salary);
END LOOP;
CLOSE emp_cursor;
END;
/
动态游标也能用:
-- 动态游标
DECLARE
TYPE emp_ref_cursor IS REF CURSOR;
v_cursor emp_ref_cursor;
v_name VARCHAR2(50);
v_sal NUMBER;
BEGIN
OPEN v_cursor FOR 'SELECT emp_name, salary FROM employees WHERE dept_id = :dept' USING 20;
LOOP
FETCH v_cursor INTO v_name, v_sal;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_name || ': ' || v_sal);
END LOOP;
CLOSE v_cursor;
END;
/
%ROWCOUNT、%NOTFOUND这些属性都能正常工作,这点确实省了不少事。
Oracle那些特有的功能,怎么处理?
这块是我花时间最多的地方。老系统里用了不少Oracle特有的东西,得一个个想办法替代。
ROWNUM分页,这个坑踩得不浅
一开始觉得ROWNUM很简单,不就是分页吗?结果发现问题一大堆:
-- Oracle里的经典写法
SELECT * FROM (
SELECT ROWNUM rn, t.*
FROM employees t
WHERE ROWNUM <= 20
) WHERE rn > 10;
在金仓里也能用ROWNUM,但是要注意一个问题:先排序再截断。
-- 正确的写法
SELECT * FROM (
SELECT ROWNUM rn, t.*
FROM (
SELECT emp_id, emp_name, salary
FROM employees
ORDER BY salary DESC
) t
WHERE ROWNUM <= 20
) WHERE rn > 10;
如果不注意这个顺序,查出来的结果可能跟预期不一样。我记得当时有个分页查询,结果老是第一页和第二页有重复数据,查了好久才发现是这个顺序问题。
后来想想,其实金仓原生的LIMIT/OFFSET更简单:
-- 金仓原生分页,更清晰
SELECT emp_id, emp_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 10;
不过老系统里已经有不少用ROWNUM的代码,要是全部改工作量太大,所以大部分还是保持原样,只改了几个有问题的地方。
CONNECT BY层次查询,还好支持得不错
老系统里有个组织架构树,用了CONNECT BY来查询层级关系:
-- Oracle里的层次查询
SELECT
LEVEL,
LPAD(' ', 2*(LEVEL-1)) || emp_name AS emp_tree,
emp_id,
parent_id
FROM employees
START WITH parent_id IS NULL
CONNECT BY PRIOR emp_id = parent_id
ORDER SIBLINGS BY emp_name;
金仓里也能直接用,语法基本一致。这个确实省了不少事,不用重写复杂的递归逻辑。
不过有个小问题要注意:数据里有循环引用的时候会报错。
-- 如果某个节点的parent_id指向自己或祖先节点,会报错
-- 解决办法是加上NOCYCLE参数
SELECT
LEVEL,
emp_name,
CONNECT_BY_ISCYCLE -- 查看哪些行有循环
FROM employees
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR emp_id = parent_id;
我发现老系统里确实有几条这样的脏数据,导致查询报错。后来加上NOCYCLE参数解决了,但是又把有问题的数据清理了一遍。
DBLINK跨库查询,这个得好好规划
老系统里用DBLINK连接几个不同的数据库做数据同步。迁移的时候一开始想把DBLINK也一起迁过来,后来想想不太对劲。
金仓是支持DBLINK的:
-- 创建数据库链接
CREATE DATABASE LINK remote_finance
CONNECT TO remote_user IDENTIFIED BY "remote_password"
USING '192.168.10.20:1521/ORCL';
-- 使用DBLINK查询
SELECT * FROM remote_schema.remote_table@remote_finance WHERE ROWNUM <= 5;
但是有几个问题要考虑:
- 性能问题:跨库查询肯定慢于本地查询
- 稳定性问题:网络一抖,整个查询就挂了
- 安全性问题:密码暴露在连接字符串里
最后我们决定改用KFS数据同步工具,把需要跨库访问的数据定期同步到本地表,然后应用直接查本地表。这样虽然多了个同步的过程,但是稳定性和性能都好很多。
性能调优,这是最头疼的一块
迁移完成不是结束,只是开始。真正上线之后,性能问题才一个接一个地冒出来。
执行计划,得重新看一遍
Oracle的执行计划在金仓里不一定能用。优化器的逻辑不一样,同一个SQL可能走完全不同的执行路径。
-- 在金仓里看执行计划
EXPLAIN ANALYZE
SELECT o.customer_id, SUM(i.qty * i.unit_price) AS amt
FROM t_order o
JOIN t_order_item i ON i.order_id = o.order_id
WHERE o.customer_id = 2001
GROUP BY o.customer_id;
我发现有些SQL在Oracle里走索引,在金仓里走全表扫描。一开始以为是索引没建对,后来发现是统计信息的问题:
-- 更新统计信息
ANALYZE TABLE employees;
更新统计信息之后,执行计划确实好了很多。所以我的建议是:迁移完成后,核心SQL都要重新看一遍执行计划,确保走对了路径。
索引策略,不能照搬
Oracle里的索引策略在金仓里不一定适用。比如Oracle里的Bitmap索引,在OLTP系统里可能不太适合直接照搬。
-- Oracle里的Bitmap索引
CREATE BITMAP INDEX idx_emp_dept ON employees(dept_id);
-- 金仓里可能更适合用B-tree索引
CREATE INDEX idx_emp_dept ON employees(dept_id);
还有这个复合索引的顺序问题:
-- 查询条件经常是 customer_id + order_time
CREATE INDEX idx_order_customer_time
ON t_order (customer_id, order_time);
-- 但是如果查询条件只有 order_time,这个索引就帮不上忙
-- 可能需要再建一个单独的索引
CREATE INDEX idx_order_time
ON t_order (order_time);
我建议根据实际的查询模式来设计索引,而不是照搬Oracle的索引设计。
分区表,迁移的时候要注意
老系统里有一些按时间分区的表,迁移的时候要注意分区的定义:
-- 创建分区表
CREATE TABLE t_order (
order_id NUMBER(12,0) NOT NULL,
customer_id NUMBER(12,0) NOT NULL,
order_time TIMESTAMP NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount NUMBER(12,2) DEFAULT 0 NOT NULL,
CONSTRAINT pk_t_order PRIMARY KEY (order_id)
) PARTITION BY RANGE (order_time) (
PARTITION p202601 VALUES LESS THAN (TO_DATE('2026-02-01', 'YYYY-MM-DD')),
PARTITION p202602 VALUES LESS THAN (TO_DATE('2026-03-01', 'YYYY-MM-DD'))
);
查询的时候要确保能利用到分区裁剪:
-- 能利用分区裁剪的查询
SELECT * FROM t_order
WHERE order_time BETWEEN '2026-01-15' AND '2026-01-20';
-- 不能利用分区裁剪的查询(对分区键做了函数操作)
SELECT * FROM t_order
WHERE EXTRACT(YEAR FROM order_time) = 2026;
我发现有些SQL因为对分区键做了函数操作,导致查询变慢。后来改了SQL写法,性能一下子就上去了。
实际迁移中的坑
说了这么多理论,最后聊聊实际迁移中遇到的一些坑。
数据类型转换的问题
有些数据类型在转换的时候容易出问题:
-- Oracle里的NUMBER类型
CREATE TABLE products (
price NUMBER(10,2)
);
-- 迁移到金仓,精度有时候会变
-- 比如某个金额字段,原来是12位整数2位小数
-- 迁移后可能变成了10位整数2位小数
-- 导致数据溢出
这个问题我发现得早,不然上线就出大问题。所以迁移前一定要仔细检查每个字段的定义,确保精度一致。
字符集的问题
Oracle里用的是ZHS16GBK编码,金仓默认是UTF8。迁移的时候如果字符集不匹配,很容易出现乱码:
-- 查看Oracle的字符集
SELECT value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';
-- 查看金仓的字符集
SHOW server_encoding;
-- 如果不一致,迁移的时候要指定编码
我当时遇到一个问题:老系统里有些特殊字符,用GBK编码能正常显示,但是转成UTF8之后就乱码了。后来一个个数据清洗了一遍才解决。
NULL和空字符串的问题
Oracle里NULL和空字符串是同一个东西,但金仓里不是:
-- Oracle里
SELECT '' IS NULL FROM dual; -- 返回 TRUE
-- 金仓里
SELECT '' IS NULL; -- 返回 FALSE
这个差异在应用代码里可能导致问题。比如应用代码判断某个字段是否为空,在Oracle里用IS NULL判断,但是迁移到金仓后,空字符串就不被视为NULL了。
解决的办法有几个:
- 在迁移的时候把空字符串都转成NULL
- 修改应用代码,同时判断NULL和空字符串
- 开启兼容参数
SET ora_input_emptystr_isnull = ON;
迁移工具,用对能省不少事
金仓提供了KDTS和KFS两个迁移工具,用好了能省不少事。
KDTS主要用于离线迁移,可以一次性把数据都迁过来:
# KDTS配置文件示例
source:
type: oracle
host: 192.168.1.100
port: 1521
database: ORCL
username: system
password: password
target:
type: kingbase
host: 192.168.1.200
port: 54321
database: ORCL
username: system
password: password
migration:
fetch_size: 1000
write_batch_size: 5000
large_table_split_threshold_rows: 1000000
KFS主要用于在线同步,适合需要实时同步数据的场景。
我觉得迁移的时候应该分阶段进行:
- 先用KDTS做一次离线迁移,把大部分数据迁过来
- 然后用KFS做增量同步,追平数据差异
- 应用切换到新库
- 运行一段时间,确保稳定后再停掉旧库
总结一下这几个星期的感受
迁移Oracle到金仓这个项目,说实话比我想象的要复杂。不是说金仓的兼容性不好,恰恰相反,兼容性确实不错,大部分东西都能直接用。但是——总是有但是——那些细节问题,那些边界情况,那些隐藏的坑,才是真正让人头疼的地方。
我觉得迁移的时候要注意这几件事:
- 别想着一步到位,分阶段迁移,慢慢来
- 仔细对账,数据迁完了要一遍遍对,确保没丢数据
- 重新看执行计划,别指望Oracle的执行计划在金仓里能用
- 多测试,核心功能要多测试,确保没改坏东西
- 留足时间,迁移比想象中要慢,要给够时间
金仓确实是个不错的选择,特别是对Oracle的兼容性这块,做得确实不错。但是迁移不是换一下数据库那么简单,需要仔细规划,认真执行,不断调整。
这几个星期确实累,但是看到系统慢慢稳定下来,性能也达到了预期,心里还是挺有成就感的。希望这些经验能给其他要做类似项目的团队一些参考吧。
金仓官网:kingbase.com.cn