背景
很多数据库问题都有一个共同特点:刚开始并不明显。
一条 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 |
sql
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)
sql
EXPLAIN (ANALYZE, BUFFERS, TIMING) 你的SQL;
在 SQL 调优过程中,执行计划是 DBA 最重要的依据。
EXPLAIN 展示的是优化器根据统计信息推测出来的执行路径,而 EXPLAIN ANALYZE
会真正执行 SQL,并反馈实际耗时、实际返回行数以及 Buffer 使用情况。
分析执行计划时,需要重点关注:
- Seq Scan:是否发生了不必要的大范围扫描;
- Index Scan:索引是否真正参与访问;
- Hash Join、Nested Loop:连接方式是否符合业务特点;
- rows 与 actual rows:优化器估算是否准确。
很多性能问题,并不是数据库能力不足,而是优化器因为错误的信息选择了不合适的路径。
1. 缺索引:从全表扫描到精准定位
现象:一个简单查询为什么需要扫描百万数据?
在订单系统中,根据客户和订单状态查询数据是非常常见的场景。
如果这类 SQL 执行越来越慢,首先需要确认数据库是不是走了正确的访问路径。
156ms → 0.041ms
sql
SELECT count(*), sum(amount)
FROM orders
WHERE cust_id = 12345 AND status = 'PAID';
orders 只有主键,没有 (cust_id, status)。
前
text
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
消耗会持续增加。
优化的核心思路不是让数据库"更努力地扫描",而是通过合理索引,让数据库尽可能少访问无关数据。
sql
CREATE INDEX idx_orders_cust_status ON orders(cust_id, status);
ANALYZE orders;
后
text
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。
2. 隐式类型转换:一个引号导致索引失效
现象:明明有主键,为什么还是慢?
很多业务系统通过 ORM 框架访问数据库,参数类型由程序自动绑定。
看似简单的字符串字段,如果传入了数字类型,数据库可能会进行隐式转换,从而改变索引访问方式。
62.5ms → 0.021ms
sql
CREATE TABLE t_code(id VARCHAR(32) PRIMARY KEY, name text);
-- 100 万行
sql
SELECT * FROM t_code WHERE id = 1000;
text
Parallel Seq Scan on t_code
Filter: ((id)::integer = 1000)
Execution Time: 62.474 ms
数据库把 id 转成 integer,主键索引用不上。
sql
SELECT * FROM t_code WHERE id = '1000';
text
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 绑参最容易错。
3. 函数包列:不要让索引失去发挥空间
现象:有索引,为什么优化器还是不用?
很多开发人员习惯直接在查询条件中处理字段,例如日期格式转换、大小写转换。
这种写法表达清晰,但可能让普通 BTree 索引无法直接匹配。
307ms → 47ms
sql
SELECT count(*)
FROM orders
WHERE date_trunc('month', order_date) = DATE '2024-06-01';
有 order_date 索引也没用:
text
Seq Scan on orders
Filter: (date_trunc('month', order_date) = '2024-06-01')
Rows Removed by Filter: 1914290
Execution Time: 307.517 ms
改成范围:
sql
SELECT count(*)
FROM orders
WHERE order_date >= DATE '2024-06-01'
AND order_date < DATE '2024-07-01';
text
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 |
| >= 且 < | Bitmap Index Scan | 47.2 ms |
列上套函数,BTree 用不上。同类:
sql
WHERE upper(name) = 'ADA'
WHERE amount + 0 = 100
WHERE to_char(dt,'YYYYMM') = '202406'
先改 SQL。业务非要表达式,再考虑表达式索引(函数须 IMMUTABLE;带时区的
date_trunc 常过不了)。
4. 统计过期:估 5 千,实 40 万
- skew_orders 先插 1000 行
- 不 ANALYZE
- 再插 200 万行
- 查 status = 'PAID'
text
Seq Scan on skew_orders
rows=5129
actual rows=400200
Execution Time: 355.146 ms
sql
ANALYZE skew_orders;
text
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。
5. 连接:点查 NestedLoop,报表 HashJoin
| 算法 | 适用场景 |
|---|---|
| Nested Loop | 外表结果集较小,内表可以通过索引点查 |
| Hash Join | 等值连接,参与连接的数据量较大 |
| Merge Join | 双方数据已有序,或能够利用索引有序扫描 |
点查:
sql
SELECT o.order_id, o.amount, i.sku, i.qty
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
WHERE o.order_id = 10001;
text
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
一周汇总:
sql
SELECT o.status, count(*), sum(i.qty)
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
WHERE o.order_date >= DATE '2024-06-01'
AND o.order_date < DATE '2024-06-08'
GROUP BY o.status;
text
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、大报表上分区或预聚合。
6. EXISTS → Hash Semi Join
sql
SELECT count(*)
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.cust_id = c.cust_id
AND o.status = 'CANCEL'
AND o.amount > 900
);
text
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 治理 | 需要加载插件 |
sql
-- enable_hint = on
SELECT /*+ IndexScan(orders idx_orders_cust_status) */ count(*)
FROM orders
WHERE 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 分析和调优能力。对于需要推进数据库国产化,同时又希望控制应用改造成本和技术学习成本的项目来说,具有较强的实际价值。