一、为什么要做这次测试
MySQL 迁国产库,真到项目里,比"兼不兼容"更要紧的是几个能不能落地的具体问题:
原来的 MySQL DDL 能不能直接建表? 业务 SQL 拿过来能不能跑? 跑出慢 SQL 之后,还能不能像在 MySQL 里一样,用索引和执行计划调下来?
这次我拿 KingbaseES V009R003C018 的 MySQL 兼容模式跑了一遍。没只建一张表插几行数据就完事,而是照着一个电商订单履约系统的样子,把建表、造数、JSON、Upsert、分页查询、执行计划和索引优化都走了一遍。
测试环境:
数据库版本:KingbaseES V009R003C018 兼容模式:database_mode = mysql 数据库端口:54321 测试库:mysql_compat_demo 测试用户:system
连上之后先确认版本和兼容模式:
select version();
show database_mode;
输出:
KingbaseES V009R003C018 database_mode = mysql
二、业务场景:电商订单履约系统
为了尽量贴近真实的 MySQL 应用,设计了 8 张业务表:
mall_customer 客户主数据表 mall_product 商品表 mall_order 订单主表 mall_order_item 订单明细表 mall_payment 支付流水表 mall_delivery 物流配送表 mall_refund 售后退款表 mall_operation_log 业务操作日志表
数据量:
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 项目里很常见的写法:
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='订单主表';
这些都建过去了:
AUTO_INCREMENT COMMENT UNIQUE KEY KEY DECIMAL DATETIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP JSON
这一段 DDL 能直接跑起来,对 MySQL 项目迁移来说,第一步的改造成本能省不少。
四、常用 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;
输出:
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
这里过的是:
LIMIT IFNULL CONCAT DATE_FORMAT ORDER BY
再试 MySQL 里常见的 Upsert:
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();
输出:
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:
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;
输出:
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:
UPDATE mall_order
SET ext_info = JSON_SET(ext_info, '$.risk_tag', 'HIGH_VALUE')
WHERE order_id = 10001;
验证:
{"client": "app", "coupon": {"amount": 0.50, "coupon_id": "CP000002"}, "campaign_id": "CAMP06", "risk_tag": "HIGH_VALUE"}
订单扩展字段、风控标签、活动参数这类 MySQL 里常见的 JSON 用法,常规的查和改基本都能覆盖。
六、一个要注意的兼容差异
测试里碰到一个值得记一下的差异。
造数的时候我一开始顺手用了 PostgreSQL 风格写日期:
TIMESTAMP '2026-01-01 00:00:00' + ((g % 180) || ' day')::interval
在 MySQL 兼容模式下这么写不行。|| 在这里走的是 MySQL 的逻辑 OR 语义,日期运算也得按 MySQL 的来。
改成这样就好了:
DATE_ADD(TIMESTAMP '2026-01-01 00:00:00', INTERVAL (g % 180) DAY)
所以做 MySQL 兼容迁移,验证的时候最好就按 MySQL 的语法习惯来,别混着别的方言写。
七、性能:一条订单查询为什么慢
语法能跑通之后,接下来就是 DBA 更在意的:跑得快不快。
要测的是运营常用的一条查询——浙江、小程序渠道、已支付成功的订单,一周内按支付金额倒序取前 20 条。
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;
优化前执行计划的关键几段:
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
问题挺清楚:
mall_order 有 50 万行。 SQL 只要前 20 条。 但没有合适的索引,库里只能先并行扫订单表,再过滤、排序、分页。
只取 20 条,却把整张大表扫了一遍。
八、索引优化:从全表扫描到索引扫描
第一版索引按过滤条件来建:
CREATE INDEX idx_order_query_hot
ON mall_order(order_status, pay_status, province_code, channel, created_at, pay_amount);
效果:
Index Scan using idx_order_query_hot Execution Time: 31.769 ms
全表扫描是去掉了,但还要排序。因为 created_at 是范围条件,而 SQL 要按 pay_amount DESC, created_at DESC 排。
第二版索引就往分页排序上调:
CREATE INDEX idx_order_query_sort_hot
ON mall_order(order_status, pay_status, province_code, channel, pay_amount DESC, created_at DESC);
优化后的执行计划:
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
前后对比:
优化前:Parallel Seq Scan + top-N Sort,67.244 ms 优化后:Index Scan + Nested Loop,0.647 ms 性能提升:约 103.9 倍
这次测下来我觉得最有用的一点就在这:MySQL 兼容模式不光是语法能跑,执行计划照样能看,组合索引照样能设计,真实业务 SQL 也照样能调下来。
九、DBA 视角:服务、备份和日志
服务这块,这个实例已经挂到 systemd 上了:
kingbase-v9r003-mysql.service active (running)
备份用 sys_dump 验了一下:
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
输出:
/home/kingbase/mall_product_backup.sql 702K -- Kingbase database dump
日志这边,启动日志能看到实例监听在 54321:
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 兼容把一整套流程走了一遍:
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 去设计索引,最后拿实测数据收尾。




