【金仓数据库征文】从 Oracle 到金仓:一次零误差的数据库国产化迁移实录

接活:一套不肯停的老系统

去年九月,我被派到某地方金融客户现场,活儿是订单交易系统的数据库国产化替换。
源库一台 Oracle 11g,单机,跑了八年。核心交易表两亿多行,存储过程加触发器小三百个。到的那天下午,客户信息中心主任在会议室把话说得很直白:信创是硬指标,年内必须切到金仓数据库,业务不能停,一分钟都不行。

我当时没吱声,心里其实打鼓。这套系统历史包袱太重,Oracle 特性用得又深又杂,谁敢拍胸脯说迁过去就完事?但活儿派下来了,硬着头皮也得上。

目录

摸底:越摸越发凉

正式动手前,我关在机房里花了两天摸源库。怎么说呢,越摸心里越发凉。

自增主键全是"序列 + 触发器"那套老办法撑着,光序列就四十多个。PL/SQL 里 NVLDECODESYSDATE 满天飞,还有几个存储过程靠 Package 级别的全局变量传值--这玩意儿我最怕,状态飘忽,排查起来要命。历史代码里隐式转换更是一抓一大把,字符串列直接跟数字 =,在 Oracle 上靠它那套宽松的类型推断将就了八年。

再就是割接窗口卡得死。客户给的窗口是凌晨两点到六点,四个小时。这四个小时里得追平增量、校验数据、切应用、跑冒烟,中间随便卡一下,天亮交易一开就是生产事故,得上报的那种。

方案:KDTS 打头,KFS 兜底

方案不复杂,我自己画了几个版本,最后定的就三步。

第一步,KDTS 做全量离线迁移,表结构、对象定义、存量数据一次性搬过来。第二步,KFS 做增量同步,存量搬完后它靠日志解析把源库持续产生的增量同步到金仓,撑起割接前的双轨运行。第三步是割接当晚,停应用、追平最后那点增量、校验数据、切连接串、起来冒烟。

KDTS 是必须上的,没得选。它支持自定义类型映射、并行迁移,搬完还能做源端和目的端的数据对比。金融场景数据一条都不能错,这功能对我来说就是定心丸。KFS 负责把割接窗口里那点增量兜住。俩工具一搭,四个小时我才敢交差。

迁移能力 KDTS KFS
定位 离线全量迁移 异构实时同步
对象定义 支持所有对象定义迁移 仅表结构和主键
同步方式 一次同步 持续同步,支持断点续传
数据对比 迁移后支持源/目的数据对比 不涉及
在迁移中的角色 主力,必用 兜底增量,支撑双轨

整个方案串起来,我画了张图,大体就这么条路:
#mermaid-svg-Gma7PpLkpybrRuCl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gma7PpLkpybrRuCl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gma7PpLkpybrRuCl .error-icon{fill:#552222;}#mermaid-svg-Gma7PpLkpybrRuCl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gma7PpLkpybrRuCl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .marker.cross{stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gma7PpLkpybrRuCl p{margin:0;}#mermaid-svg-Gma7PpLkpybrRuCl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label text{fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label span{color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label span p{background-color:transparent;}#mermaid-svg-Gma7PpLkpybrRuCl .label text,#mermaid-svg-Gma7PpLkpybrRuCl span{fill:#333;color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .node rect,#mermaid-svg-Gma7PpLkpybrRuCl .node circle,#mermaid-svg-Gma7PpLkpybrRuCl .node ellipse,#mermaid-svg-Gma7PpLkpybrRuCl .node polygon,#mermaid-svg-Gma7PpLkpybrRuCl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .rough-node .label text,#mermaid-svg-Gma7PpLkpybrRuCl .node .label text,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label,#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gma7PpLkpybrRuCl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .rough-node .label,#mermaid-svg-Gma7PpLkpybrRuCl .node .label,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label,#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label{text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .node.clickable{cursor:pointer;}#mermaid-svg-Gma7PpLkpybrRuCl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .arrowheadPath{fill:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gma7PpLkpybrRuCl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gma7PpLkpybrRuCl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster text{fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster span{color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gma7PpLkpybrRuCl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape p,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label rect,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gma7PpLkpybrRuCl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gma7PpLkpybrRuCl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段三 割接
停应用追平增量
数据校验
切连接串
冒烟上线
阶段二 迁移
KDTS 全量搬迁
结构+对象+存量
KFS 增量同步
双轨运行对账
阶段一 评估
源库摸底
兼容性扫描

动手之前我先干了一件事--跑扫描脚本,把源库里可能踩雷的写法先揪出来。这步现在回想起来觉得特别值,省了后面不知道多少返工:

sql 复制代码
-- 迁移前:扫描源库存储过程/函数源码里的兼容性风险点
-- 拿 all_source 视图,用正则匹配几类常见雷区
WITH risk_patterns AS (
    SELECT '隐式转换(字符串列=数字)'  AS risk, '\b(\w+_no|\w+_code|\w+_id)\s*=\s*[0-9]' AS pattern
    UNION ALL
    SELECT 'NULLIF第一参为NULL风险',    'NULLIF\s*\(\s*NULL'  AS pattern
    UNION ALL
    SELECT '索引列上套了函数',          'WHERE\s+(UPPER|LOWER|SUBSTR|TO_CHAR)\s*\(' AS pattern
    UNION ALL
    SELECT '依赖Package全局变量',       '\w+\.(get|set)_\w+\s*\(' AS pattern
    UNION ALL
    SELECT '外连接(+)老写法',           '\(+\)' AS pattern
)
SELECT s.owner, s.name, s.type, s.line, r.risk,
       LEFT(s.text, 120) AS snippet
FROM all_source s
JOIN risk_patterns r ON s.text ~ r.pattern
WHERE s.owner NOT IN ('SYS','SYSTEM','SYSMAN')
  AND s.type IN ('PROCEDURE','FUNCTION','PACKAGE','PACKAGE BODY','TRIGGER')
ORDER BY s.owner, s.name, s.line;

一跑出来三百多条。我盯着那个结果列表看了半天,心凉了半截,但好歹是提前知道了雷在哪,对着清单逐个过,比两眼一抹黑迁完再排查强太多。

真刀真枪:四个让我后背发凉的瞬间

方案归方案,真迁起来坑是一个接一个往外蹦。我挑四个印象最深的讲。

坑一:DATE 类型把时分秒弄丢了

这坑差点出事。

割接前那个周三晚上,我在校验数据,账面金额对上了,正准备收拾东西回家。业务方一个电话打过来,说订单明细按时间段导出的报表,时间全是零点,没法看了。

我心里咯噔一下,困意全没了,赶紧回去查。

问题出在一张历史表上。Oracle 的 DATE 类型带时分秒,金仓数据库的 date 只精确到天。KDTS 默认会把 Oracle 的 DATE 映射成金仓的 timestamp,这规则本身没毛病。可偏偏这张表当年是手写的 DDL,负责迁移的同事图省事,照着字面 DATE 就建成了 date,数据一灌进去,时分秒全被截成 00:00:00

好在发现得早,改列类型,重灌这张表,过去了。

但这事给我留了个教训,刻挺深:Oracle 的 DATE 和金仓的 date,名字一模一样,精度差着一截。类型映射必须盯死,别凭字面想当然。

sql 复制代码
-- 修复:把误建成 date 的列改回 timestamp,时分别再丢
ALTER TABLE order_detail
    ALTER COLUMN create_time TYPE TIMESTAMP WITHOUT TIME ZONE
    USING create_time::TIMESTAMP WITHOUT TIME ZONE;

-- 顺手建个函数索引,把"按天查"也照顾到,省得以后又有人在索引列上套 ::date
CREATE INDEX IF NOT EXISTS idx_order_detail_day
    ON order_detail ((create_time::DATE));

坑二:序列自增主键的水土不服

Oracle 那套"序列 + 触发器"自增方案,迁过来本身能跑。金仓数据库对序列、触发器,连 DUAL 表、ROWNUM 伪列都做了兼容,这点挺省心。

但坑不在数据库,在应用代码里。

有些老 SQL 显式调了 seq_order.nextval 拿主键,迁过来序列还在,接着用没毛病。可有个存储过程的触发器逻辑写得有 bug,新库里 nextval 取出来的值跟已有数据撞了,主键冲突直接报错。我查了半天,最后发现是触发器里 SELECT ... INTO 没处理好空值--老库上数据巧了没踩到,新库一换分布就现形了。

我本来想在老路上缝缝补补,改改触发器得了。后来一琢磨不行,这玩意儿以后就是个雷,指不定哪天又炸。索性把核心交易表的自增主键统一改成 IDENTITY 列,触发器那套老逻辑整个请出去。

这里有个关键点容易翻车:新序列的起始值得接上老序列当前值,不然接着往上发还是会撞。我第一次没注意,建完表插数据就报主键冲突,愣了一秒才反应过来。

sql 复制代码
-- 把"序列+触发器"老方案整体迁到 IDENTITY 列
-- 重点:IDENTITY 起始值要接上老序列当前值,否则主键撞车

-- 1. 先看老序列走到哪了
SELECT last_number FROM user_sequences WHERE sequence_name = 'SEQ_ORDER';
-- 假设查出来是 200003456

-- 2. 建新表,id 直接用 IDENTITY,起始值接老序列 +1
CREATE TABLE orders_new (
    id          BIGINT GENERATED ALWAYS AS IDENTITY (START WITH 200003457) PRIMARY KEY,
    order_no    VARCHAR(32)  NOT NULL,
    amount      NUMERIC(18,2) NOT NULL DEFAULT 0,
    create_time TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 3. 老数据原样导入,主键沿用老 id
INSERT INTO orders_new (id, order_no, amount, create_time)
SELECT id, order_no, amount, create_time FROM orders;

-- 4. 把序列推到比当前最大 id 更靠后的位置,留 1000 的缓冲
SELECT setval(pg_get_serial_sequence('orders_new','id'),
              (SELECT MAX(id) FROM orders_new) + 1000, true);

-- 5. 确认无误,丢掉老触发器,切表名
DROP TRIGGER IF EXISTS trg_order ON orders;
ALTER TABLE orders      RENAME TO orders_bak;
ALTER TABLE orders_new  RENAME TO orders;

-- 6. 验一下自增是不是接着老 id 往下发
INSERT INTO orders_new (order_no, amount) VALUES ('SMOKE_001', 0.01)
RETURNING id;

坑三:隐式转换把索引干废了

这坑跟类型有关,但比第一个隐蔽。

订单表 order_noVARCHAR,应用那边图省事,前端传进来的数字直接拼进 SQL:

sql 复制代码
-- 别这么写:字符串列跟数字比,靠隐式转换对齐
SELECT * FROM orders WHERE order_no = 20240724001;

这写法在 Oracle 上跑了八年,相安无事。迁到金仓以后开始作妖--时灵时不灵,执行计划一会走索引一会全表扫,同一个接口测十次结果都不一定一样。

根子还是隐式转换。类型没对死,优化器在不同数据分布下拿的主意不一样,列一旦被函数包一层,索引就废了,跟没建一样。

改起来其实不难,难在改的是全应用的拼接逻辑,量大,几百个接口。我跟开发负责人商量了一下,分两步走:高频接口先改成参数化、类型对死,当周必须搞完;低频的排期慢慢改。底下这段是应用层该有的样子,参数绑死类型,别再让数据库去猜:

java 复制代码
// 应用层:别拼 SQL,老老实实用 PreparedStatement 绑参
// 类型在绑定时就定死,隐式转换那点破事彻底没有可乘之机
public Order findByOrderNo(String orderNo) {
    String sql = "SELECT id, order_no, amount, create_time "
               + "FROM orders WHERE order_no = ?";   // 字符串占位符
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, orderNo);                    // 绑死成 VARCHAR
        try (ResultSet rs = ps.executeQuery()) {
            return rs.next() ? mapRow(rs) : null;
        }
    } catch (SQLException e) {
        throw new RuntimeException("查询订单失败 orderNo=" + orderNo, e);
    }
}

坑四:NULLIF 一个不起眼的参数差异

这坑特别小,但真实存在。

NULLIF(value1, value2) 这函数,金仓数据库允许 value1NULL,Oracle 不允许。就差这一点。

有个报表存储过程里写了类似 NULLIF(NULL, 0) 的逻辑。在 Oracle 上这个分支压根跑不到,所以八年了没人发现。迁过来以后行为变了,报表对不上,业务方又来找我。

这种点太细了,KDTS 不会替你发现,工具再强也管不到这种参数级别的语义差异。没办法,只能靠存储过程逐个回归测。后来我让团队专门整了份"函数差异清单",把这种参数层面的细微差别逐条标出来,回归的时候照着过一遍。

类型映射和数据校验:最该抠的两个地方

把前面这些坑归纳一下,迁移里最该抠的就两件事,一个是数据类型映射,一个是数据校验。

先说类型映射。这张表是我们在现场反复核对后定下来的规则,贴出来给后来人参考:

Oracle 类型 金仓数据库类型 迁移注意点
NUMBER(p,s) numeric(p,s) 精度标度照搬,别丢
VARCHAR2(n) varchar(n) 字符/字节长度语义要确认
DATE timestamp Oracle 的 DATE 带时分秒,必须映成 timestamp
CLOB / NCLOB clob 大对象,注意存储过程里 DBMS_LOB 调用
BLOB blob 二进制大对象
RAW bytea 二进制串
LONG text 建议趁机改成 text

再说数据校验。KDTS 自带源端和目的端的数据对比,但金融场景我不敢只信工具,又额外写了套校验脚本,对核心表做行数、金额合计、主键范围三项校验。割接当晚就靠它拍板。整个校验切换的流程,我画了下:
#mermaid-svg-AKJtquKEsdReX2UN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AKJtquKEsdReX2UN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AKJtquKEsdReX2UN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AKJtquKEsdReX2UN .error-icon{fill:#552222;}#mermaid-svg-AKJtquKEsdReX2UN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AKJtquKEsdReX2UN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .marker.cross{stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AKJtquKEsdReX2UN p{margin:0;}#mermaid-svg-AKJtquKEsdReX2UN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label text{fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label span{color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label span p{background-color:transparent;}#mermaid-svg-AKJtquKEsdReX2UN .label text,#mermaid-svg-AKJtquKEsdReX2UN span{fill:#333;color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .node rect,#mermaid-svg-AKJtquKEsdReX2UN .node circle,#mermaid-svg-AKJtquKEsdReX2UN .node ellipse,#mermaid-svg-AKJtquKEsdReX2UN .node polygon,#mermaid-svg-AKJtquKEsdReX2UN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .rough-node .label text,#mermaid-svg-AKJtquKEsdReX2UN .node .label text,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label,#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label{text-anchor:middle;}#mermaid-svg-AKJtquKEsdReX2UN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .rough-node .label,#mermaid-svg-AKJtquKEsdReX2UN .node .label,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label,#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label{text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .node.clickable{cursor:pointer;}#mermaid-svg-AKJtquKEsdReX2UN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .arrowheadPath{fill:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AKJtquKEsdReX2UN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AKJtquKEsdReX2UN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .cluster text{fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster span{color:#333;}#mermaid-svg-AKJtquKEsdReX2UN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AKJtquKEsdReX2UN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN rect.text{fill:none;stroke-width:0;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape,#mermaid-svg-AKJtquKEsdReX2UN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape p,#mermaid-svg-AKJtquKEsdReX2UN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label rect,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AKJtquKEsdReX2UN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AKJtquKEsdReX2UN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



02:00 停应用写入
KFS 追平最后增量
启动三项数据校验
行数比对
金额合计比对
主键范围比对
三项全平?
切 JDBC 连接串至金仓
定位差异表
回滚或重灌该表
冒烟测试
通过?
06:00 前对外放量

底下就是那段校验脚本,通过 DBLink 连源端,逐表对三项:

sql 复制代码
-- 割接当晚:自动化数据校验脚本
-- 通过 DBLink(src_link) 连源端,逐表对 行数/金额/主键范围 三项
DO $$
DECLARE
    v_tbl      TEXT;
    v_src_cnt  BIGINT;  v_dst_cnt  BIGINT;
    v_src_sum  NUMERIC; v_dst_sum  NUMERIC;
    v_src_min  BIGINT;  v_dst_min  BIGINT;
    v_src_max  BIGINT;  v_dst_max  BIGINT;
    v_diff     BIGINT;
BEGIN
    FOREACH v_tbl IN ARRAY ARRAY['orders','order_detail','payment','refund'] LOOP
        -- ① 行数
        EXECUTE format('SELECT COUNT(*) FROM %I',           v_tbl) INTO v_dst_cnt;
        EXECUTE format('SELECT COUNT(*) FROM src_link.%I',  v_tbl) INTO v_src_cnt;
        v_diff := v_dst_cnt - v_src_cnt;
        RAISE NOTICE '% 行数  源=%  目的=%  差异=%', v_tbl, v_src_cnt, v_dst_cnt, v_diff;
        IF v_diff <> 0 THEN
            RAISE EXCEPTION '% 行数不一致,差 %,立即停止割接', v_tbl, v_diff;
        END IF;

        -- ② 金额合计(一分钱都不能差)
        EXECUTE format('SELECT COALESCE(SUM(amount),0) FROM %I',          v_tbl) INTO v_dst_sum;
        EXECUTE format('SELECT COALESCE(SUM(amount),0) FROM src_link.%I', v_tbl) INTO v_src_sum;
        RAISE NOTICE '% 金额  源=%  目的=%  差额=%', v_tbl, v_src_sum, v_dst_sum, v_dst_sum - v_src_sum;
        IF ABS(v_dst_sum - v_src_sum) > 0.01 THEN
            RAISE EXCEPTION '% 金额不平,差 %', v_tbl, v_dst_sum - v_src_sum;
        END IF;

        -- ③ 主键范围
        EXECUTE format('SELECT MIN(id),MAX(id) FROM %I',          v_tbl) INTO v_dst_min, v_dst_max;
        EXECUTE format('SELECT MIN(id),MAX(id) FROM src_link.%I', v_tbl) INTO v_src_min, v_src_max;
        IF v_src_min <> v_dst_min OR v_src_max <> v_dst_max THEN
            RAISE EXCEPTION '% 主键范围对不上,可能漏迁或重迁 (源 %..% / 目的 %..%)',
                            v_tbl, v_src_min, v_src_max, v_dst_min, v_dst_max;
        END IF;
    END LOOP;
    RAISE NOTICE '===== 全部校验通过,可以切应用 =====';
END $$;

应用层切换也顺带说一句。JDBC 驱动换成金仓的,连接串和 Oracle 模式下的方言基本对得上。但有几个参数得调,prepareThreshold 和批次大小,这俩对批量插入性能影响挺大,我调了好几轮才找到合适的值:

java 复制代码
// 金仓 JDBC 连接串(Oracle 兼容模式)
// prepareThreshold 控制几次执行后切换到服务端预编译,批量场景调大点
String url = "jdbc:kingbase8://10.0.0.12:54321/ks_db"
           + "?currentSchema=trade"
           + "&prepareThreshold=5"
           + "&defaultRowPrefetch=100";

// 批量插入别一条一条 round-trip,攒批一把送
try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO orders(order_no, amount, create_time) VALUES(?,?,?)")) {
    conn.setAutoCommit(false);
    for (Order o : batch) {
        ps.setString(1, o.getOrderNo());
        ps.setBigDecimal(2, o.getAmount());
        ps.setTimestamp(3, Timestamp.from(o.getCreateTime()));
        ps.addBatch();
    }
    ps.executeBatch();
    conn.commit();
}

落地效果

前后跑了整一个月双轨。割接当晚四个小时内收工,核心交易表两亿行数据,零误差落库。我记得校验脚本最后打出"全部校验通过"那行 NOTICE 的时候,机房里几个人都长出一口气。

落地后的效果大致这样:

指标 迁移前(Oracle) 迁移后(金仓数据库)
核心查询平均响应 120ms 95ms
批量入库吞吐 基准 提升约 30%(调优后)
割接窗口 - 4 小时内完成,零数据误差
双轨运行 KFS 增量同步,割接前持续对账
存储过程兼容度 - 约 92% 直接可用,余下需适配

性能能稳中有升,说实话有点出乎我意料。后来想了想,很大程度上是迁移时顺手把那堆隐式转换、SELECT *、深分页的老毛病一起治了。说白了迁移就是个体检机会,趁着重构把历史技术债清一清,比单纯把数据搬过去值钱多了。

写在最后

干完这一票,我对"数据库迁移"这四个字的理解彻底变了。

它真不是把数据从 A 库导到 B 库那么简单,而是一次对整套系统的体检和重构。最容易栽跟头的,从来不是表结构、存储过程这种大块头--那些 KDTS 基本都能接住。真正咬人的,是 DATE 丢精度、隐式转换废索引、NULLIF 参数差一个 NULL 这种"名字差不多、行为差一截"的细节。

后来再碰到迁移后业务报异常,我基本都是按下面这棵决策树往下捋,八九不离十能快速定位:
#mermaid-svg-0tKcGP8hlc1OGCI7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0tKcGP8hlc1OGCI7 .error-icon{fill:#552222;}#mermaid-svg-0tKcGP8hlc1OGCI7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0tKcGP8hlc1OGCI7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .marker.cross{stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0tKcGP8hlc1OGCI7 p{margin:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label text{fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label span{color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label span p{background-color:transparent;}#mermaid-svg-0tKcGP8hlc1OGCI7 .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 span{fill:#333;color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node rect,#mermaid-svg-0tKcGP8hlc1OGCI7 .node circle,#mermaid-svg-0tKcGP8hlc1OGCI7 .node ellipse,#mermaid-svg-0tKcGP8hlc1OGCI7 .node polygon,#mermaid-svg-0tKcGP8hlc1OGCI7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .rough-node .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .rough-node .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label{text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node.clickable{cursor:pointer;}#mermaid-svg-0tKcGP8hlc1OGCI7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .arrowheadPath{fill:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster text{fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster span{color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0tKcGP8hlc1OGCI7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape p,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label rect,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0tKcGP8hlc1OGCI7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 时间全零点/报表错乱
主键冲突/写入报错
查询时灵时不灵
报表数值对不上
迁移后业务异常
症状?
DATE 类型映射

查是否误映成 date
序列起始值与触发器

查 IDENTITY 接续
隐式转换

查列与参数类型
函数参数差异

对照差异清单回归
改 timestamp 重灌
setval 推进 + 改 IDENTITY
应用参数化绑类型
逐存储过程回归

几条心得,留给后来人。

类型映射一定逐表逐列过,别信字面,尤其 DATE 和字符长度语义,这俩是重灾区。隐式转换这种历史债,迁移时能清就清,别留给生产去炸,到时候半夜被叫起来排查更难受。存储过程的函数差异 KDTS 发现不了,老老实实做回归测试,对照差异清单过。双轨期别省那点成本,KFS 增量同步加数据校验,是割接当晚敢按下回车键的底气。

最后说句实在话,金仓数据库在 Oracle 兼容上做得确实到位,序列、触发器、DUAL、ROWNUM 这些老特性基本都能接住,省了我们大量改造成本。但兼容归兼容,该较真的细节一个都不能放过。迁移这事,慢就是快,前头把坑踩明白了,后头割接才不至于手忙脚乱。

相关推荐
0566462 小时前
RAG 向量检索:从“查字“到“查意“
数据库·人工智能·学习·oracle
bug嘛我经常写2 小时前
dbf文件UTF-8转GBK编码,以及两编码文件互转
数据库·python
知行合一。。。2 小时前
LangGraph--03--本地服务与 Studio 调试
数据库
你不是我我2 小时前
【AI 测评】PostgreSQL主从流复制实战:数据同步、状态验证与故障切换
数据库·postgresql
月落归舟2 小时前
Redis 三种消息队列实现方案
数据库·redis·list
棒棒的唐2 小时前
postgresql集成pgvector
数据库·postgresql
智购科技智能售货柜2 小时前
自动售货机商品识别YOLO模型训练实战:从6万张图片到98%识别率的完整复盘~YH
运维·服务器·数据库·人工智能·redis·物联网·yolo
ACP广源盛139246256733 小时前
2026 PCIe互连芯片@ACP#国产替代格局解析:芯动科技领跑高端交换芯片赛道
大数据·网络·数据库·人工智能·分布式·嵌入式硬件
代码代码快快显灵3 小时前
MYSQL-DAY2
数据库·mysql