从Oracle到金仓:一场让人又爱又恨的迁移实战

21 阅读12分钟

兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点

说真的,接这个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;

但是有几个问题要考虑:

  1. 性能问题:跨库查询肯定慢于本地查询
  2. 稳定性问题:网络一抖,整个查询就挂了
  3. 安全性问题:密码暴露在连接字符串里

最后我们决定改用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了。

解决的办法有几个:

  1. 在迁移的时候把空字符串都转成NULL
  2. 修改应用代码,同时判断NULL和空字符串
  3. 开启兼容参数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主要用于在线同步,适合需要实时同步数据的场景。

我觉得迁移的时候应该分阶段进行:

  1. 先用KDTS做一次离线迁移,把大部分数据迁过来
  2. 然后用KFS做增量同步,追平数据差异
  3. 应用切换到新库
  4. 运行一段时间,确保稳定后再停掉旧库

总结一下这几个星期的感受

迁移Oracle到金仓这个项目,说实话比我想象的要复杂。不是说金仓的兼容性不好,恰恰相反,兼容性确实不错,大部分东西都能直接用。但是——总是有但是——那些细节问题,那些边界情况,那些隐藏的坑,才是真正让人头疼的地方。

我觉得迁移的时候要注意这几件事:

  1. 别想着一步到位,分阶段迁移,慢慢来
  2. 仔细对账,数据迁完了要一遍遍对,确保没丢数据
  3. 重新看执行计划,别指望Oracle的执行计划在金仓里能用
  4. 多测试,核心功能要多测试,确保没改坏东西
  5. 留足时间,迁移比想象中要慢,要给够时间

金仓确实是个不错的选择,特别是对Oracle的兼容性这块,做得确实不错。但是迁移不是换一下数据库那么简单,需要仔细规划,认真执行,不断调整。

这几个星期确实累,但是看到系统慢慢稳定下来,性能也达到了预期,心里还是挺有成就感的。希望这些经验能给其他要做类似项目的团队一些参考吧。


金仓官网:kingbase.com.cn