暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

从 MySQL 到 KingbaseES:一次业务 SQL 兼容性与性能优化的实测

一、为什么要做这次测试

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 去设计索引,最后拿实测数据收尾。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论