欢迎光临
我们一直在努力

关系数据库替换用金仓:从 Oracle 到 KingbaseES 的迁移实战

我写这篇不是“产品介绍”,而是一份替换项目的开发者笔记:哪些地方能省心,哪些地方最好提前踩刹车。文里所有 SQL/PLSQL 片段我都按“复制到 ksql 就能跑”来准备,把 Oracle 迁移里最常见的一些改造点,尽量用一条能执行的路线串起来。

文章目录

    • 1. 为什么我会把“替换”拆成四件事
    • 2. 一张图说清迁移流水线(我习惯这么落地)
    • 3. 环境与前置:Windows 上用 ksql 快速搭一个“可跑通”的实验场
      • 3.1 连接与“Oracle 模式自检”
    • 4. 数据类型映射:我最关心的不是“能不能建表”,而是“语义有没有悄悄变”
    • 5. 实操:用一个“订单系统”把表、索引、分区、序列跑通
      • 5.1 一键初始化(可复制粘贴执行)
    • 6. Oracle 特有功能的替代实现:我最常写的三类改造
      • 6.1 ROWNUM:能用,但要小心“先排序还是先截断”
      • 6.2 CONNECT BY:层次查询能接住,建议把递归逻辑测试到边界
      • 6.3 DBLINK:跨库访问能用,但我建议先定清“你要的是查询还是同步”
    • 7. 存储过程、函数、游标:我更喜欢“先小步跑通,再规模化迁移”
      • 7.1 一个可跑通的过程:计算订单金额 + 记录审计(自治事务)
      • 7.2 游标迁移:显式游标 + %ROWCOUNT 的常见模式
    • 8. 性能调优:我对比的不只是“快不快”,而是“怎么定位”
      • 8.1 用执行计划把“猜测”变成“证据”
      • 8.2 用 HINT 做“可控对比”(别拿它当长期拐杖)
      • 8.3 分区与统计信息:先把“能命中”做对
    • 我在迁移复盘里最常写的“踩坑清单”

d7ac40c0b69a1d7e02d11496b0aafafa.jpg


1. 为什么我会把“替换”拆成四件事

把 Oracle 换成金仓(KingbaseES),大家第一反应往往是“兼容性到底行不行”。但真到项目里,最要命的不是“能不能跑”,而是“怎么稳稳交付”。说白了:兼容性帮你少改代码,不会替你把项目做完。所以我习惯先把替换拆成四件事:

  • 语法与对象:SQL、视图、序列、触发器、包/过程/函数是否能接住;
  • 数据类型:NUMBER/DATE/CLOB 这类“老朋友”到底怎么落地,精度和语义是否一致;
  • 运行与工具链:脚本怎么跑、怎么回归、怎么定位慢 SQL;
  • 上线风险:回滚策略、灰度、双写/对账窗口怎么设计。
  • 手册里对 Oracle 兼容覆盖面写得很细:在 Oracle 模式下,数据类型、伪列、DML/DQL、过程化语言、触发器、动态 SQL 这些常见能力都在兼容范围里,连 DBLink 这种偏“工程向”的能力也给到了。这篇文章我就按“现场真的会踩”的路子来写:先把数据类型落稳,再把过程/函数跑通,最后把 ROWNUM、CONNECT BY、DBLINK 这些高频点用可执行的方式验证一遍。


    2. 一张图说清迁移流水线(我习惯这么落地)

    #mermaid-svg-hUPPPjdRSDoD3cjv{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-hUPPPjdRSDoD3cjv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hUPPPjdRSDoD3cjv .error-icon{fill:#552222;}#mermaid-svg-hUPPPjdRSDoD3cjv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hUPPPjdRSDoD3cjv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hUPPPjdRSDoD3cjv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hUPPPjdRSDoD3cjv .marker.cross{stroke:#333333;}#mermaid-svg-hUPPPjdRSDoD3cjv svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hUPPPjdRSDoD3cjv p{margin:0;}#mermaid-svg-hUPPPjdRSDoD3cjv .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster-label text{fill:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster-label span{color:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster-label span p{background-color:transparent;}#mermaid-svg-hUPPPjdRSDoD3cjv .label text,#mermaid-svg-hUPPPjdRSDoD3cjv span{fill:#333;color:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv .node rect,#mermaid-svg-hUPPPjdRSDoD3cjv .node circle,#mermaid-svg-hUPPPjdRSDoD3cjv .node ellipse,#mermaid-svg-hUPPPjdRSDoD3cjv .node polygon,#mermaid-svg-hUPPPjdRSDoD3cjv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hUPPPjdRSDoD3cjv .rough-node .label text,#mermaid-svg-hUPPPjdRSDoD3cjv .node .label text,#mermaid-svg-hUPPPjdRSDoD3cjv .image-shape .label,#mermaid-svg-hUPPPjdRSDoD3cjv .icon-shape .label{text-anchor:middle;}#mermaid-svg-hUPPPjdRSDoD3cjv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hUPPPjdRSDoD3cjv .rough-node .label,#mermaid-svg-hUPPPjdRSDoD3cjv .node .label,#mermaid-svg-hUPPPjdRSDoD3cjv .image-shape .label,#mermaid-svg-hUPPPjdRSDoD3cjv .icon-shape .label{text-align:center;}#mermaid-svg-hUPPPjdRSDoD3cjv .node.clickable{cursor:pointer;}#mermaid-svg-hUPPPjdRSDoD3cjv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hUPPPjdRSDoD3cjv .arrowheadPath{fill:#333333;}#mermaid-svg-hUPPPjdRSDoD3cjv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hUPPPjdRSDoD3cjv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hUPPPjdRSDoD3cjv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hUPPPjdRSDoD3cjv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hUPPPjdRSDoD3cjv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hUPPPjdRSDoD3cjv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster text{fill:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv .cluster span{color:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv 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-hUPPPjdRSDoD3cjv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hUPPPjdRSDoD3cjv rect.text{fill:none;stroke-width:0;}#mermaid-svg-hUPPPjdRSDoD3cjv .icon-shape,#mermaid-svg-hUPPPjdRSDoD3cjv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hUPPPjdRSDoD3cjv .icon-shape p,#mermaid-svg-hUPPPjdRSDoD3cjv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hUPPPjdRSDoD3cjv .icon-shape rect,#mermaid-svg-hUPPPjdRSDoD3cjv .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hUPPPjdRSDoD3cjv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hUPPPjdRSDoD3cjv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hUPPPjdRSDoD3cjv :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    源库盘点对象/SQL/数据量/依赖

    问题词归类类型/语法/过程/运维

    目标库准备Oracle模式实例+参数基线

    结构迁移schema/表/索引/分区/序列

    数据迁移全量->校验->增量窗口

    SQL改写与兼容验证ROWNUM/CONNECT BY/外连接/包

    性能回归执行计划/索引/统计信息

    上线与回滚预案灰度/双写/对账

    我不太建议一上来就“全量自动迁”。更稳的做法是先把风险收敛:挑一个模块或一组核心表,把类型、过程、慢 SQL 这三件事跑通,手里有了可复用的套路再扩面。


    3. 环境与前置:Windows 上用 ksql 快速搭一个“可跑通”的实验场

    文章环境是 Windows 且在 ksql 上操作,下面所有演示默认满足:

    • Windows 已安装 KingbaseES,并创建了 Oracle 模式实例
    • 你能在 PowerShell / Windows Terminal 中执行 ksql

    如果你的环境这一步就卡住了(比如端口不通、服务没起来、ksql 连不上),我之前写过一篇从安装到连通性验证的笔记,按步骤走基本不会翻车: Windows 安装 KingbaseES V9R1C10 与 Oracle 兼容特性实战

    概念解释我就不展开了,先用能跑通的方式把环境确认下来:只要你在 ksql 里把几个关键语法跑起来,后面所有实操才有意义。

    3.1 连接与“Oracle 模式自检”

    ksql -h 127.0.0.1 -p 54321 -U system -d test

    端口这块按你的实例来,我这边 Oracle 兼容版用的是 54322,所以截图里会看到 54322。 image.png image.png

    进到 test=# 之后,先做三件事:

    SELECT CURRENT_USER;
    SELECT CURRENT_TIMESTAMP;

    — Oracle 习惯的 dual 伪表(能跑通,基本说明你在 Oracle 语义环境里)
    SELECT CURRENT_DATE FROM dual;

    image.png

    如果你接下来还能跑通 ROWNUM 和 CONNECT BY,基本可以认为“Oracle 迁移的高频语法”处在可验证范围内(第 6 章会给例子)。


    4. 数据类型映射:我最关心的不是“能不能建表”,而是“语义有没有悄悄变”

    类型映射这事,最容易被“建表不报错”给骗了。表是建起来了没错,但语义一旦偏了,后面就是慢慢对账、慢慢掉坑。真实项目里,类型风险我基本都栽在这三类上:

    • 精度:NUMBER(p,s) 的范围、金额字段的四舍五入;
    • 时间语义:DATE 到底带不带时间(应用是否依赖隐含时间);
    • 大对象与字符集:CLOB/NCLOB、VARCHAR2/NVARCHAR2 的边界值与编码。

    KingbaseES 的手册里有类型对照表(比如 numeric/number 对 Oracle NUMBER 的对应关系等)我自己在项目里常用的“落地映射建议”大概是这样:

    Oracle 常见类型迁移落地建议(KingbaseES Oracle 模式)我会额外检查的点
    NUMBER(p,s) 优先保持为 NUMBER(p,s) / NUMERIC(p,s) 金额字段是否存在隐含精度扩展
    VARCHAR2(n) VARCHAR(n) 字段长度边界、是否依赖空串/空值差异
    CHAR(n) CHAR(n) 尾部空格比较规则
    DATE DATE(Oracle 语义环境下更贴近原用法) 是否有“只存日期不存时间”的约定
    TIMESTAMP TIMESTAMP 时区列是否需要 TIMESTAMP WITH TIME ZONE
    CLOB/NCLOB CLOB/NCLOB 大字段更新是否走 DBMS_LOB 路线
    BLOB BLOB 事务与定位器用法

    我自己的经验是:别在迁移第一天就“顺手把 NUMBER 全改成 INT”。你省下来的那点存储空间,后面很容易以“线上对账差 0.01”的形式加倍还你。


    5. 实操:用一个“订单系统”把表、索引、分区、序列跑通

    下面这套脚本可以直接在 ksql 里执行:建表、建索引、建序列、建分区表、插数据。一口气跑完,后面第 6、7、8 章的兼容验证和性能定位就能顺着做下去。

    5.1 一键初始化(可复制粘贴执行)

    — 清理(如存在)
    DROP TABLE IF EXISTS t_order_item;
    DROP TABLE IF EXISTS t_order;
    DROP TABLE IF EXISTS t_order_audit;
    DROP SEQUENCE IF EXISTS seq_order_id;

    — 序列(Oracle项目里常见:主键来自 sequence)
    CREATE SEQUENCE seq_order_id START WITH 100001 INCREMENT BY 1;

    — 订单主表:按下单时间做范围分区(示例:按月)
    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,
    remark VARCHAR(200),
    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'))
    );

    — 明细表
    CREATE TABLE t_order_item (
    item_id NUMBER(12,0) NOT NULL,
    order_id NUMBER(12,0) NOT NULL,
    sku VARCHAR(32) NOT NULL,
    qty NUMBER(12,0) NOT NULL,
    unit_price NUMBER(12,2) NOT NULL,
    CONSTRAINT pk_t_order_item PRIMARY KEY (item_id)
    );

    CREATE INDEX idx_t_order_item_order_id ON t_order_item(order_id);
    CREATE INDEX idx_t_order_customer_time ON t_order(customer_id, order_time);

    — 审计表(演示自治事务/日志)
    CREATE TABLE t_order_audit (
    audit_id NUMBER(12,0) NOT NULL,
    order_id NUMBER(12,0),
    action VARCHAR(30) NOT NULL,
    action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    detail VARCHAR(200),
    CONSTRAINT pk_t_order_audit PRIMARY KEY (audit_id)
    );
    CREATE SEQUENCE seq_audit_id START WITH 1 INCREMENT BY 1;

    — 插入一些订单
    INSERT INTO t_order(order_id, customer_id, order_time, status, remark, total_amount)
    VALUES (seq_order_id.NEXTVAL, 2001, TO_TIMESTAMP('2026-01-15 10:12:00','YYYY-MM-DD HH24:MI:SS'), 'NEW', NULL, 0);
    INSERT INTO t_order(order_id, customer_id, order_time, status, remark, total_amount)
    VALUES (seq_order_id.NEXTVAL, 2002, TO_TIMESTAMP('2026-02-03 09:45:10','YYYY-MM-DD HH24:MI:SS'), 'PAID', '年度合同首单', 0);

    — 插入明细(手工写几条,后面也会演示用 CONNECT BY 批量造数)
    INSERT INTO t_order_item(item_id, order_id, sku, qty, unit_price)
    SELECT 1, (SELECT MIN(order_id) FROM t_order), 'SKU-001', 2, 99.50 FROM dual;
    INSERT INTO t_order_item(item_id, order_id, sku, qty, unit_price)
    SELECT 2, (SELECT MIN(order_id) FROM t_order), 'SKU-002', 1, 199.00 FROM dual;
    INSERT INTO t_order_item(item_id, order_id, sku, qty, unit_price)
    SELECT 3, (SELECT MAX(order_id) FROM t_order), 'SKU-003', 3, 39.90 FROM dual;

    COMMIT;

    验证一下对象是否都在:

    SELECT COUNT(*) AS order_cnt FROM t_order;
    SELECT COUNT(*) AS item_cnt FROM t_order_item;


    6. Oracle 特有功能的替代实现:我最常写的三类改造

    6.1 ROWNUM:能用,但要小心“先排序还是先截断”

    ROWNUM 伪列是为了兼容 Oracle 而加的,手册里也专门提醒了一个经典坑:带 ORDER BY 的时候,ROWNUM 的结果会受取数顺序影响

    在项目里我一般这么写 Top-N:

    — 先排序,再截断
    SELECT *
    FROM (
    SELECT order_id, customer_id, order_time, total_amount
    FROM t_order
    ORDER BY order_time DESC
    )
    WHERE ROWNUM <= 10;

    如果你更偏标准写法,也可以直接用 ORDER BY … LIMIT …。但在迁移阶段,我通常先求“老 SQL 能稳定跑”,等核心链路稳了,再慢慢把写法往更标准的方向收敛。

    6.2 CONNECT BY:层次查询能接住,建议把递归逻辑测试到边界

    层次查询这块,我更关心的是“能不能把老逻辑原样接住”。手册里也写得比较明确:层次查询用于父子关系数据的层次返回,KingbaseES 与 Oracle 都支持,语义上是兼容的

    下面用一个组织结构做演示(同时把“造测试数据”的方法也带上):

    DROP TABLE IF EXISTS t_dept;
    CREATE TABLE t_dept(
    dept_id NUMBER(12,0) PRIMARY KEY,
    parent_id NUMBER(12,0),
    dept_name VARCHAR(50) NOT NULL
    );

    INSERT INTO t_dept VALUES (1, NULL, '总部');
    INSERT INTO t_dept VALUES (2, 1, '研发中心');
    INSERT INTO t_dept VALUES (3, 1, '销售中心');
    INSERT INTO t_dept VALUES (4, 2, '平台组');
    INSERT INTO t_dept VALUES (5, 2, '应用组');
    COMMIT;

    — 层次查询:从总部开始展开
    SELECT
    LPAD(' ', (LEVEL1)*2) || dept_name AS dept_tree,
    dept_id,
    parent_id,
    LEVEL
    FROM t_dept
    START WITH dept_id = 1
    CONNECT BY PRIOR dept_id = parent_id
    ORDER SIBLINGS BY dept_id;

    迁移项目里我会专门加两类用例,提前把隐患揪出来:

    • 循环引用(parent_id 指回自己/祖先)能否被及时发现;
    • 层级很深(>100)时性能与排序是否符合预期。

    6.3 DBLINK:跨库访问能用,但我建议先定清“你要的是查询还是同步”

    在 Oracle 模式下,KingbaseES 的兼容能力中包含 DBLink 这类高级能力,官网文章里也给了 DBLink 在 Oracle 兼容模式下实现跨库实时访问/同步的思路

    我在项目里会先问一句:你要的是“跨库查询”还是“跨库同步”。这会影响你是把它当作运行时依赖,还是把它当作数据集成链路的一环。

    下面给一个“能读懂也好改”的例子(远端连接信息按你环境替换):

    — 示例:创建数据库链接(语法风格与 Oracle 接近)
    — 具体参数与权限要求以你版本的手册/规范为准
    CREATE DATABASE LINK lk_remote_finance
    CONNECT TO remote_user IDENTIFIED BY "remote_password"
    USING '192.168.10.20:1521/ORCL';

    — 使用方式(示意):通过 @dblink 访问远端对象
    SELECT * FROM remote_schema.remote_table@lk_remote_finance WHERE ROWNUM <= 5;

    我的工程建议是:

    • 业务强依赖跨库查询时,一定要做超时、重试、熔断(否则一次网络抖动就能拖垮核心链路)。
    • 如果只是迁移过渡期的数据比对/对账,更推荐做异步同步 + 本地查询,把线上查询从跨库依赖里拿掉。

    7. 存储过程、函数、游标:我更喜欢“先小步跑通,再规模化迁移”

    在 Oracle 模式下,过程化语言语法基础、静态/动态 SQL、存储过程/函数、匿名块、触发器这些能力都覆盖了。我在迁移里更喜欢走“小步快跑”:先挑一个低耦合、能验证闭环的过程跑通(最好带事务、日志、异常),再把迁移规范固化下来,然后再规模化推进。

    7.1 一个可跑通的过程:计算订单金额 + 记录审计(自治事务)

    下面这个过程做两件事:

  • 汇总明细金额写回主表;
  • 用自治事务写审计日志(主事务回滚也不影响日志)。
  • PRAGMA AUTONOMOUS_TRANSACTION、DBMS_OUTPUT 这类用法,在不少 Oracle 项目里都很常见。用它们做个小闭环验证,基本能快速判断:你这个版本的过程能力到底能不能接住迁移诉求。

    先开输出:

    SET SERVEROUTPUT ON;

    创建过程:

    CREATE OR REPLACE PROCEDURE p_recalc_order_total(p_order_id NUMBER)
    IS
    v_total NUMBER(12,2);

    PRAGMA AUTONOMOUS_TRANSACTION;
    v_audit_id NUMBER(12,0);
    BEGIN
    SELECT COALESCE(SUM(qty * unit_price), 0)
    INTO v_total
    FROM t_order_item
    WHERE order_id = p_order_id;

    UPDATE t_order
    SET total_amount = v_total
    WHERE order_id = p_order_id;

    SELECT seq_audit_id.NEXTVAL INTO v_audit_id FROM dual;
    INSERT INTO t_order_audit(audit_id, order_id, action, detail)
    VALUES (v_audit_id, p_order_id, 'RECALC_TOTAL', 'total=' || v_total);

    COMMIT;

    DBMS_OUTPUT.PUT_LINE('订单' || p_order_id || '已回写总金额=' || v_total);
    END;
    /

    执行验证(你可以把订单号换成你实际生成的):

    SELECT order_id, total_amount FROM t_order ORDER BY order_id;

    BEGIN
    p_recalc_order_total((SELECT MIN(order_id) FROM t_order));
    END;
    /

    SELECT order_id, total_amount FROM t_order ORDER BY order_id;
    SELECT * FROM t_order_audit ORDER BY audit_id;

    迁移项目里我会在这一层加两类断言:

    • 明细为空时是否写回 0;
    • 大订单(明细 10w 行)下汇总性能是否满足 SLA。

    7.2 游标迁移:显式游标 + %ROWCOUNT 的常见模式

    这类代码在报表、批处理里很常见,迁移验证时我会用它来确认“游标控制流是不是一致”:

    DECLARE
    CURSOR c_orders IS
    SELECT order_id, customer_id
    FROM t_order
    ORDER BY order_id;

    v_order_id t_order.order_id%TYPE;
    v_customer_id t_order.customer_id%TYPE;
    BEGIN
    OPEN c_orders;
    LOOP
    FETCH c_orders INTO v_order_id, v_customer_id;
    EXIT WHEN c_orders%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE('order_id=' || v_order_id || ', customer=' || v_customer_id);
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('rows=' || c_orders%ROWCOUNT);
    CLOSE c_orders;
    END;
    /


    8. 性能调优:我对比的不只是“快不快”,而是“怎么定位”

    替换项目里,“慢”本身其实没那么可怕,真正难受的是:线上报了慢 SQL,你盯着 SQL 文本看半天,还是不知道该从哪里下手。我的经验很朴素:只要你能稳定拿到执行计划(最好带实际执行耗时)、看得懂索引有没有用上、分区有没有裁剪到位,调优就不会变成玄学。

    8.1 用执行计划把“猜测”变成“证据”

    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;

    image.png

    这条 SQL 我跑出来的计划里,有几个细节挺“对味”的

    • t_order 是分区表,所以计划里出现了 Append,而且两条分区分别走了各自的索引扫描:t_order_p202601_customer_id_order_time_idx 和 t_order_p202602_customer_id_order_time_idx。这点很关键,它说明按 customer_id = 2001 的过滤条件,优化器确实愿意用索引去“定位少量数据”,而不是把整张表扫一遍。
    • t_order_item 这边是 Seq Scan,别看到顺序扫描就慌。这里我的明细表就几行数据,顺扫反而更省事。真实项目里如果明细表很大,那就要盯紧:是不是缺索引、或者 join 条件写法导致索引用不上。
    • 聚合部分走的是 Hash Join + GroupAggregate,而且 Planning Time 大概 3.484 ms、Execution Time 大概 0.991 ms。对我这种“为了验证链路是否跑通”的小数据集来说,这个量级就够了;到大数据量时,我反而更关注节点类型有没有跑偏(比如该走索引却变成全表扫、该裁剪分区却全分区扫描)。

    8.2 用 HINT 做“可控对比”(别拿它当长期拐杖)

    很多 Oracle 项目里 HINT 用得特别重,这在迁移阶段很正常:你得先把核心链路“钉住”,别让执行计划飘来飘去。我的用法是:先拿 HINT 做一个对照实验,确认它在这个版本/这个写法下确实生效,再决定要不要保留到上线阶段(能不长期背着就不背着)

    你可以这样做一个“对照实验”:

    EXPLAIN ANALYZE
    SELECT /*+ INDEX(t_order idx_t_order_customer_time) */
    order_id, order_time, total_amount
    FROM t_order
    WHERE customer_id = 2001
    ORDER BY order_time DESC;

    e0919ab280608d1de22d0e2e4f3f0db8.png

    结果其实很能说明问题:它直接走了 Index Scan Backward,而且同样是按分区做 Append,计划里没出现额外的 Sort 节点。对 ORDER BY order_time DESC 这种查询来说,这通常意味着索引顺序刚好能满足排序需求,优化器就省掉了排序开销。Planning Time 大概 2.230 ms、Execution Time 大概 0.116 ms,至少在这个数据集下,路径是清晰且稳定的。

    所以我的建议还是那句话:迁移早期用 HINT 兜住“稳定可控”没问题,但后面要尽量把 SQL 和索引配合好,让优化器自己也能选到这条路。否则你会发现:业务一扩表、统计信息一变化,HINT 可能反而成了隐患。

    8.3 分区与统计信息:先把“能命中”做对

    分区这块我一般不靠“感觉”,而是先查一遍分区边界是不是你以为的那样。比如下面这条查询,能直接把 T_ORDER 的分区名和上界拉出来:

    SELECT partition_name, high_value
    FROM ALL_TAB_PARTITIONS
    WHERE table_name = 'T_ORDER';

    8e15fd7525be27cc5009c02772493ef4.png

    从图中结果看,P202601 和 P202602 的 high_value 分别是 2026-02-01 00:00:00、2026-03-01 00:00:00,跟我们建表时按月切分的预期一致。确认了这个,再谈“分区裁剪”才有意义。

    调优时我通常先做两件事:

    • 先把查询条件写得足够“可裁剪”(比如按时间范围过滤时,尽量让条件落在分区键上,并且避免对分区键做函数包裹);
    • 再按真实访问模式看索引策略:是更偏“按客户+时间取最近 N 条”,还是“按订单号精确查明细”,索引该怎么组合,别光照着模板建。

    我在迁移复盘里最常写的“踩坑清单”

  • ROWNUM + ORDER BY:Top-N 一定要子查询包起来(不然结果会“看起来没错但不稳定”)
  • 类型别乱改:NUMBER/DATE/LOB 先求语义一致,再考虑优化。
  • 过程先跑通最小闭环:先做 1 个可验证过程(带日志/异常/事务),再批量迁移。
  • 跨库别当本地用:DBLink 能解决“能不能接”,但解决不了“链路抖动”和“稳定性设计”。
  • 调优要靠证据:把 EXPLAIN ANALYZE、索引命中、分区裁剪这些能力跑通,替换才可控。
  • 赞(0)
    未经允许不得转载:171主机测评 » 关系数据库替换用金仓:从 Oracle 到 KingbaseES 的迁移实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址