【金仓数据库产品体验官】从 MySQL 到 KingbaseES:一次业务 SQL 兼容性与性能优化的实测

一、为什么要做这次测试

MySQL 迁国产库,真到项目里,比"兼不兼容"更要紧的是几个能不能落地的具体问题:

text 复制代码
原来的 MySQL DDL 能不能直接建表?
业务 SQL 拿过来能不能跑?
跑出慢 SQL 之后,还能不能像在 MySQL 里一样,用索引和执行计划调下来?

这次我拿 KingbaseES V009R003C018 的 MySQL 兼容模式跑了一遍。没只建一张表插几行数据就完事,而是照着一个电商订单履约系统的样子,把建表、造数、JSON、Upsert、分页查询、执行计划和索引优化都走了一遍。

测试环境:

text 复制代码
数据库版本:KingbaseES V009R003C018
兼容模式:database_mode = mysql
数据库端口:54321
测试库:mysql_compat_demo
测试用户:system

连上之后先确认版本和兼容模式:

sql 复制代码
select version();
show database_mode;

输出:

text 复制代码
KingbaseES V009R003C018
database_mode = mysql

二、业务场景:电商订单履约系统

为了尽量贴近真实的 MySQL 应用,设计了 8 张业务表:

text 复制代码
mall_customer       客户主数据表
mall_product        商品表
mall_order          订单主表
mall_order_item     订单明细表
mall_payment        支付流水表
mall_delivery       物流配送表
mall_refund         售后退款表
mall_operation_log  业务操作日志表

数据量:

text 复制代码
mall_customer       100000 行
mall_product         10000 行
mall_order          500000 行
mall_order_item    1500000 行
mall_payment        500000 行
mall_delivery       475000 行
mall_refund          80000 行
mall_operation_log 1000000 行

加起来四百多万行,量够了,执行计划的变化看得出来。

三、MySQL 风格 DDL

拿订单主表举例,DDL 用的就是 MySQL 项目里很常见的写法:

sql 复制代码
CREATE TABLE mall_order (
    order_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_no VARCHAR(32) NOT NULL,
    customer_id BIGINT NOT NULL,
    shop_id BIGINT NOT NULL,
    order_status VARCHAR(20) NOT NULL,
    pay_status VARCHAR(20) NOT NULL,
    order_amount DECIMAL(12,2) NOT NULL,
    discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
    pay_amount DECIMAL(12,2) NOT NULL,
    province_code VARCHAR(12) NOT NULL,
    city_code VARCHAR(12) NOT NULL,
    channel VARCHAR(20) NOT NULL,
    ext_info JSON,
    created_at DATETIME NOT NULL,
    paid_at DATETIME,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_order_no(order_no),
    KEY idx_customer_time(customer_id, created_at)
) COMMENT='订单主表';

这些都建过去了:

text 复制代码
AUTO_INCREMENT
COMMENT
UNIQUE KEY
KEY
DECIMAL
DATETIME
TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
JSON

这一段 DDL 能直接跑起来,对 MySQL 项目迁移来说,第一步的改造成本能省不少。

四、常用 SQL

先试了一条典型的订单分页查询:

sql 复制代码
SELECT order_id,
       order_no,
       IFNULL(pay_amount, 0) AS pay_amount_checked,
       CONCAT(province_code, '-', city_code) AS area_code,
       DATE_FORMAT(created_at, '%Y-%m-%d') AS order_date
FROM mall_order
WHERE order_status = 'PAID'
ORDER BY created_at DESC
LIMIT 5;

输出:

text 复制代码
order_id | order_no            | pay_amount_checked | area_code      | order_date
431999   | O202606290000431999 | 2740.00            | 110000-110100  | 2026-06-29
86399    | O202606290000086399 | 2194.00            | 110000-110100  | 2026-06-29
345599   | O202606290000345599 | 556.00             | 110000-110100  | 2026-06-29
172799   | O202606290000172799 | 1648.00            | 110000-110100  | 2026-06-29
259019   | O202606290000259019 | 948.00             | 110000-110100  | 2026-06-29

这里过的是:

text 复制代码
LIMIT
IFNULL
CONCAT
DATE_FORMAT
ORDER BY

再试 MySQL 里常见的 Upsert:

sql 复制代码
INSERT INTO mall_product(product_id, product_code, product_name, category_code, sale_price, stock_qty, status, updated_at)
VALUES (1001, 'P00001001', '无线鼠标-兼容测试', 'CAT_TEST', 99.00, 500, 'ONLINE', NOW())
ON DUPLICATE KEY UPDATE
    product_name = VALUES(product_name),
    sale_price = VALUES(sale_price),
    stock_qty = VALUES(stock_qty),
    updated_at = NOW();

输出:

text 复制代码
product_id | product_code | product_name      | sale_price | stock_qty
1001       | P00001001    | 无线鼠标-兼容测试 | 99.00      | 500

ON DUPLICATE KEY UPDATE 能直接用,这点挺要紧------商品、库存、配置、缓存落库这些表,很多都是这么写的。

五、JSON

订单表里有个 ext_info JSON 字段,用来放订单的扩展信息:客户端、活动 ID、优惠券这些。

查 JSON:

sql 复制代码
SELECT order_id,
       order_no,
       JSON_EXTRACT(ext_info, '$.client') AS client_type,
       JSON_EXTRACT(ext_info, '$.coupon.amount') AS coupon_amount
FROM mall_order
WHERE JSON_EXTRACT(ext_info, '$.client') = 'miniapp'
LIMIT 5;

输出:

text 复制代码
order_id | order_no            | client_type | coupon_amount
479524   | O202601050000479524 | "miniapp"   | 2.00
479528   | O202601090000479528 | "miniapp"   | 4.00
479532   | O202601130000479532 | "miniapp"   | 6.00
479536   | O202601170000479536 | "miniapp"   | 8.00
479540   | O202601210000479540 | "miniapp"   | 10.00

改 JSON:

sql 复制代码
UPDATE mall_order
SET ext_info = JSON_SET(ext_info, '$.risk_tag', 'HIGH_VALUE')
WHERE order_id = 10001;

验证:

text 复制代码
{"client": "app", "coupon": {"amount": 0.50, "coupon_id": "CP000002"}, "campaign_id": "CAMP06", "risk_tag": "HIGH_VALUE"}

订单扩展字段、风控标签、活动参数这类 MySQL 里常见的 JSON 用法,常规的查和改基本都能覆盖。

六、一个要注意的兼容差异

测试里碰到一个值得记一下的差异。

造数的时候我一开始顺手用了 PostgreSQL 风格写日期:

sql 复制代码
TIMESTAMP '2026-01-01 00:00:00' + ((g % 180) || ' day')::interval

在 MySQL 兼容模式下这么写不行。|| 在这里走的是 MySQL 的逻辑 OR 语义,日期运算也得按 MySQL 的来。

改成这样就好了:

sql 复制代码
DATE_ADD(TIMESTAMP '2026-01-01 00:00:00', INTERVAL (g % 180) DAY)

所以做 MySQL 兼容迁移,验证的时候最好就按 MySQL 的语法习惯来,别混着别的方言写。

七、性能:一条订单查询为什么慢

语法能跑通之后,接下来就是 DBA 更在意的:跑得快不快。

要测的是运营常用的一条查询------浙江、小程序渠道、已支付成功的订单,一周内按支付金额倒序取前 20 条。

sql 复制代码
SELECT o.order_id,
       o.order_no,
       o.customer_id,
       c.customer_name,
       o.province_code,
       o.city_code,
       o.channel,
       o.pay_amount,
       o.created_at
FROM mall_order o
JOIN mall_customer c ON c.customer_id = o.customer_id
WHERE o.order_status = 'PAID'
  AND o.pay_status = 'SUCCESS'
  AND o.province_code = '330000'
  AND o.channel = 'MINIAPP'
  AND o.created_at >= '2026-06-01'
  AND o.created_at < '2026-06-08'
ORDER BY o.pay_amount DESC, o.created_at DESC
LIMIT 20;

优化前执行计划的关键几段:

text 复制代码
Parallel Seq Scan on public.mall_order o
  Rows Removed by Filter: 165920
  Buffers: shared hit=227 read=16440

Sort Key: o.pay_amount DESC NULLS LAST, o.created_at DESC NULLS LAST
Sort Method: top-N heapsort

Execution Time: 67.244 ms

问题挺清楚:

text 复制代码
mall_order 有 50 万行。
SQL 只要前 20 条。
但没有合适的索引,库里只能先并行扫订单表,再过滤、排序、分页。

只取 20 条,却把整张大表扫了一遍。

八、索引优化:从全表扫描到索引扫描

第一版索引按过滤条件来建:

sql 复制代码
CREATE INDEX idx_order_query_hot
ON mall_order(order_status, pay_status, province_code, channel, created_at, pay_amount);

效果:

text 复制代码
Index Scan using idx_order_query_hot
Execution Time: 31.769 ms

全表扫描是去掉了,但还要排序。因为 created_at 是范围条件,而 SQL 要按 pay_amount DESC, created_at DESC 排。

第二版索引就往分页排序上调:

sql 复制代码
CREATE INDEX idx_order_query_sort_hot
ON mall_order(order_status, pay_status, province_code, channel, pay_amount DESC, created_at DESC);

优化后的执行计划:

text 复制代码
Limit  (actual time=0.280..0.627 rows=20 loops=1)
  -> Nested Loop  (actual time=0.279..0.624 rows=20 loops=1)
       -> Index Scan using idx_order_query_sort_hot on public.mall_order o
            Index Cond: order_status = 'PAID'
                        AND pay_status = 'SUCCESS'
                        AND province_code = '330000'
                        AND channel = 'MINIAPP'
                        AND created_at >= '2026-06-01'
                        AND created_at < '2026-06-08'
            Buffers: shared hit=21 read=3

Execution Time: 0.647 ms

前后对比:

text 复制代码
优化前:Parallel Seq Scan + top-N Sort,67.244 ms
优化后:Index Scan + Nested Loop,0.647 ms
性能提升:约 103.9 倍

这次测下来我觉得最有用的一点就在这:MySQL 兼容模式不光是语法能跑,执行计划照样能看,组合索引照样能设计,真实业务 SQL 也照样能调下来。

九、DBA 视角:服务、备份和日志

服务这块,这个实例已经挂到 systemd 上了:

text 复制代码
kingbase-v9r003-mysql.service active (running)

备份用 sys_dump 验了一下:

bash 复制代码
sys_dump -h 127.0.0.1 -p 54321 -U system -d mysql_compat_demo -t mall_product -f /home/kingbase/mall_product_backup.sql

输出:

text 复制代码
/home/kingbase/mall_product_backup.sql 702K
-- Kingbase database dump

日志这边,启动日志能看到实例监听在 54321:

text 复制代码
listening on IPv4 address "0.0.0.0", port 54321
listening on IPv6 address "::", port 54321
Future log output will appear in directory "sys_log".

服务管理、备份、日志路径这几样都还算清楚,纳到日常运维里没什么障碍。

十、小结

这次围绕 MySQL 兼容把一整套流程走了一遍:

text 复制代码
MySQL 风格 DDL 建表
百万级业务数据构造
LIMIT / IFNULL / CONCAT / DATE_FORMAT 查询
ON DUPLICATE KEY UPDATE 写入
JSON_EXTRACT / JSON_SET 验证
慢 SQL 执行计划分析
组合索引优化
服务、备份、日志验证

结论是:KingbaseES V009R003C018 的 MySQL 兼容模式,对常见的 MySQL 业务开发习惯覆盖得还不错。订单、商品、支付、日志这类系统,DDL、DML、分页、函数、JSON、Upsert 都可以拿来做迁移验证的重点。

不过有一点也得说清楚:兼容能跑通,离真正上生产还有距离。执行计划、索引策略、写入成本、备份恢复、运维接入,这些都还得一项项过。

这次那条订单查询从 67.244 ms 调到 0.647 ms,也说明在 MySQL 兼容模式下,原来那套 DBA 的优化办法照样管用:先看执行计划,再围着过滤条件、排序条件和 LIMIT 去设计索引,最后拿实测数据收尾。