很多数据库问题都有一个共同特点:刚开始并不明显。
一条 SQL在测试环境几十毫秒,看起来没有问题;但随着业务增长,数据量从几万增长到百万、千万级别后,隐藏的问题会逐渐放大。最终用户感知到的就是页面变慢、接口超时,甚至核心业务受到影响。
遇到 SQL 性能问题时,很多人的第一反应是增加CPU、内存,或者打开并行。但在实际运维过程中,更多的问题来自 SQL本身和优化器选择:
缺少合适索引,导致数据库扫描大量无关数据;
SQL 写法导致索引无法使用,例如对列进行函数处理;
数据变化后统计信息没有及时更新,优化器无法准确判断数据分布;
大范围查询场景下,连接算法选择不合理。
因此,一次完整的 SQL 优化过程应该是:
发现慢 SQL → 分析执行计划 → 找到根因 → 进行最小调整 → 验证优化效果 →固化经验。
本文基于 KingbaseES V9 Oracle 兼容版,通过 6组典型场景,记录从问题发现到优化验证的完整过程。
| 项 | 值 |
|---|---|
| 版本 | KingbaseES V009R002C014 |
| 模式 | initdb -m oracle,database_mode=oracle |
| 确认 | SELECT * FROM dual;、nvl() 可用 |
| 并行 | max_parallel_workers_per_gather = 0 |
customers(cust_id PK, cust_name, region, level_no, created_at) orders(order_id PK, cust_id, status, amount, order_date, remark) order_items(item_id PK, order_id, sku, qty, price)
EXPLAIN (ANALYZE, BUFFERS, TIMING) 你的SQL;
在 SQL 调优过程中,执行计划是 DBA 最重要的依据。
EXPLAIN 展示的是优化器根据统计信息推测出来的执行路径,而 EXPLAIN ANALYZE 会真正执行 SQL,并反馈实际耗时、实际返回行数以及 Buffer 使用情况。
分析执行计划时,需要重点关注:
Seq Scan:是否发生了不必要的大范围扫描;
Index Scan:索引是否真正参与访问;
Hash Join、Nested Loop:连接方式是否符合业务特点;
rows 与 actual rows:优化器估算是否准确。
很多性能问题,并不是数据库能力不足,而是优化器因为错误的信息选择了不合适的路径。
在订单系统中,根据客户和订单状态查询数据是非常常见的场景。
如果这类 SQL 执行越来越慢,首先需要确认数据库是不是走了正确的访问路径。
156ms → 0.041ms
SELECT count(*), sum(amount)FROM ordersWHERE cust_id = 12345 AND status = 'PAID';
orders 只有主键,没有 (cust_id, status)。
前
Aggregate (actual time=155.990..155.992) -> Seq Scan on orders (actual time=155.982) Filter: ((cust_id = 12345) AND (status = 'PAID')) Rows Removed by Filter: 2000000 Buffers: shared hit=64 read=15936 Execution Time: 156.023 ms
从执行计划可以看到,数据库为了找到少量目标订单,不得不读取整个 orders 表。
这种情况在数据量较小时并不明显,但随着订单规模增长,全表扫描带来的 IO 消耗会持续增加。
优化的核心思路不是让数据库”更努力地扫描”,而是通过合理索引,让数据库尽可能少访问无关数据。
CREATE INDEX idx_orders_cust_status ON orders(cust_id, status); ANALYZE orders;
后
Aggregate (actual time=0.019..0.020) -> Index Scan using idx_orders_cust_status on orders (actual time=0.015) Index Cond: ((cust_id = 12345) AND (status = 'PAID')) Buffers: shared hit=1 read=2 Execution Time: 0.041 ms
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 时间 | 156.0 ms | 0.041 ms |
| 扫描方式 | Seq Scan | Index Scan |
| Buffer | read≈15936 | hit=1,read=2 |
等值过滤列建联合索引,选择率高的列靠前。建完立刻 ANALYZE。上线前用 EXPLAIN 看 Index Cond。
很多业务系统通过 ORM 框架访问数据库,参数类型由程序自动绑定。
看似简单的字符串字段,如果传入了数字类型,数据库可能会进行隐式转换,从而改变索引访问方式。
62.5ms → 0.021ms
CREATE TABLE t_code(id VARCHAR(32) PRIMARY KEY, name text);-- 100 万行
SELECT * FROM t_code WHERE id = 1000;
Parallel Seq Scan on t_code Filter: ((id)::integer = 1000) Execution Time: 62.474 ms
数据库把 id 转成 integer,主键索引用不上。
SELECT * FROM t_code WHERE id = '1000';
Index Scan using t_code_pkey on t_code Index Cond: ((id)::text = '1000'::text) Execution Time: 0.021 ms
| 写法 | 执行计划 | 时间 |
|---|---|---|
| id = 1000 | Parallel Seq Scan | 62.5 ms |
| id = ‘1000’ | Index Scan | 0.021 ms |
字符列传字符,数值列传数值。ORM 绑参最容易错。
很多开发人员习惯直接在查询条件中处理字段,例如日期格式转换、大小写转换。
这种写法表达清晰,但可能让普通 BTree 索引无法直接匹配。
307ms → 47ms
SELECT count(*)FROM ordersWHERE date_trunc('month', order_date) = DATE '2024-06-01';
有 order_date 索引也没用:
Seq Scan on orders Filter: (date_trunc('month', order_date) = '2024-06-01') Rows Removed by Filter: 1914290 Execution Time: 307.517 ms
改成范围:
SELECT count(*)FROM ordersWHERE order_date >= DATE '2024-06-01' AND order_date < DATE '2024-07-01';
Bitmap Index Scan on idx_orders_date Index Cond: (order_date >= ... AND order_date < ...) Execution Time: 47.213 ms
| 写法 | 执行计划 | 时间 |
|---|---|---|
| date_trunc(列) = 常量 | Seq Scan | 307.5 ms |
| >= 且 <1 | Bitmap Index Scan | 47.2 ms |
列上套函数,BTree 用不上。同类:
WHERE upper(name) = 'ADA'WHERE amount + 0 = 100WHERE to_char(dt,'YYYYMM') = '202406'
先改 SQL。业务非要表达式,再考虑表达式索引(函数须 IMMUTABLE;带时区的 date_trunc 常过不了)。
skew_orders 先插 1000 行
不 ANALYZE
再插 200 万行
查 status = ‘PAID’
Seq Scan on skew_orders rows=5129 actual rows=400200 Execution Time: 355.146 ms
ANALYZE skew_orders;
Seq Scan on skew_orders rows=396932 actual rows=400200 Execution Time: 161.686 ms
过滤比例约 20%,继续 Seq Scan 合理。基数从 5129 回到 396932。多表连接时,这个偏差会改 NestedLoop 和 HashJoin 的选择。
大批量导入、删除、更新后,对关键表跑 ANALYZE。别默认 autovacuum 立刻跟上。读计划时固定对比 rows 和 actual rows。
| 算法 | 适用场景 |
|---|---|
| Nested Loop | 外表结果集较小,内表可以通过索引点查 |
| Hash Join | 等值连接,参与连接的数据量较大 |
| Merge Join | 双方数据已有序,或能够利用索引有序扫描 |
点查:
SELECT o.order_id, o.amount, i.sku, i.qtyFROM orders oJOIN order_items i ON i.order_id = o.order_idWHERE o.order_id = 10001;
Nested Loop (actual time=0.046..0.058) -> Index Scan on orders (order_id = 10001) -> Index Scan on order_items (order_id = 10001) Execution Time: 0.076 ms
一周汇总:
SELECT o.status, count(*), sum(i.qty)FROM orders oJOIN order_items i ON i.order_id = o.order_idWHERE o.order_date >= DATE '2024-06-01' AND o.order_date < DATE '2024-06-08'GROUP BY o.status;
HashAggregate -> Hash Join Hash Cond: (i.order_id = o.order_id) -> Seq Scan on order_items -> Hash -> Bitmap Heap Scan on orders Execution Time: 923.385 ms
约 2 万订单建 Hash,再探测 500 万明细。
可继续做:过滤列有索引、投影收窄、HINT 试 NestedLoop、大报表上分区或预聚合。
SELECT count(*)FROM customers cWHERE EXISTS ( SELECT 1 FROM orders o WHERE o.cust_id = c.cust_id AND o.status = 'CANCEL' AND o.amount > 900);
Aggregate -> Hash Semi Join Hash Cond: (c.cust_id = o.cust_id) -> Seq Scan on customers -> Hash -> Seq Scan on orders Filter: amount > 900 AND status = 'CANCEL' Execution Time: 335.900 ms
相关 EXISTS 变成 Semi Join。内层仍全表扫,整体还是 300ms+。
建 orders(cust_id, status, amount),让内表尽量索引过滤,再看计划是否变成 Nested Loop Semi Join。
| 优先级 | 手段 | 适用场景 | 本文结果 |
|---|---|---|---|
| P0 | 联合索引 | 高频等值、范围查询 | 156 ms → 0.041 ms |
| P0 | 改写 SQL | 列上函数、隐式类型转换 | 307 ms → 47 ms |
| P0 | ANALYZE | 大批量 DML 后 | 估算基数 5 千 → 约 40 万 |
| P1 | work_mem | Hash、Sort 出现磁盘溢写 | 本组 5 千分组差异较小 |
| P1 | HINT | 执行计划明显不合理 | 已验证 |
| P2 | 分区、物化、并行 | 大表报表查询 | 本文未展开 |
| P2 | sys_sqltune | 批量 SQL 治理 | 需要加载插件 |
-- enable_hint = onSELECT /*+ IndexScan(orders idx_orders_cust_status) */ count(*)FROM ordersWHERE cust_id = 999 AND status = 'NEW';
HINT 只止血。根因落在索引、统计、SQL 上。
锁定 SQL:RT、CPU、IO
EXPLAIN (ANALYZE, BUFFERS),对比 rows 与 actual
三类:缺索引、统计旧、SQL 不可索引
最小变更:先 SQL/索引,后参数/HINT
同数据复测时间与 buffers
固化:索引列序、禁止 函数(列)、批量后 ANALYZE
| 测试项目 | 结果 |
|---|---|
| 缺索引点查 | 156 ms → 0.041 ms |
| 隐式类型转换 | 62.5 ms → 0.021 ms |
| 函数包列 | 307 ms → 47 ms |
| 统计信息过期 | 估算 5,129 行,实际 400,200 行 |
| 点查 Join | Nested Loop,0.076 ms |
| 一周报表 Join | Hash Join,923 ms |
| EXISTS | Hash Semi Join,336 ms |
6 组 SQL 做成回归脚本。升级、改参、大批量导入后各跑一遍。
通过本次测试可以看到,KingbaseES V9 Oracle 兼容版不仅能够兼容常见的 Oracle 语法和函数,在 SQL 执行计划分析、索引使用、统计信息维护以及连接方式选择等方面,也延续了传统关系型数据库较为成熟的优化思路。
从实际使用体验来看,Oracle 技术人员迁移到 KingbaseES 后,很多既有经验仍然可以继续发挥作用。例如:
可以使用 dual、nvl 等常见 Oracle 语法和函数;
可以通过 EXPLAIN ANALYZE 分析 SQL 的真实执行情况;
可以结合索引、统计信息和连接算法定位性能问题;
可以通过 HINT 对执行计划进行干预和验证;
Oracle DBA 熟悉的 SQL 调优思路,在 KingbaseES 中依然具有较高的参考价值。
这意味着,对于已经长期使用 Oracle 的企业来说,KingbaseES 并不是一套完全陌生的数据库体系。无论是开发人员进行 SQL 适配,还是 DBA 开展性能分析,都能够在原有经验基础上较快完成技术迁移。
当然,兼容并不代表所有 SQL 都可以不经验证直接迁移。不同数据库在优化器实现、数据类型处理、函数特性以及执行计划细节上仍然可能存在差异。因此,在实际迁移和上线过程中,仍然需要结合具体版本、业务模型和数据规模进行充分测试。
总体来看,KingbaseES V9 Oracle 兼容版在降低 Oracle 应用迁移门槛方面表现较为成熟。它既保留了较多 Oracle 用户熟悉的使用习惯,也提供了较完整的 SQL 分析和调优能力。对于需要推进数据库国产化,同时又希望控制应用改造成本和技术学习成本的项目来说,具有较强的实际价值。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/23825935/viewspace-3178298/ ,如需转载,请注明出处,否则将追究法律责任。