做业务开发或运维的同学,肯定遇到过这样的痛点:明明昨天还跑得很顺的SQL,今天突然变卡;或者同样的查询,小数据量时没问题,数据量一上来就超时。其实解决这些问题的核心钥匙,就是达梦数据库的「执行计划」。今天就用最通俗的话,带新手朋友吃透执行计划,搞定常见的生产慢查询。
很多新手听到「执行计划」会觉得高深,其实它就是数据库的「操作说明书」。当你提交一条SQL时,数据库不会直接执行,而是先由「优化器」分析出多种执行方案,然后选一个成本最低的(比如CPU、IO消耗最少),这个最优方案就是执行计划。
执行计划里有两个核心信息必须看懂,新手记住这两点就够了:
操作符:表示数据库具体做了什么操作,比如全表扫描、索引扫描等,是判断SQL效率的关键;
代价三元组:格式是(代价,记录行数,字节数),比如(5,1000,8000),分别代表执行成本、处理的行数和数据量,数字越小效率越高。
首先要解决「怎么看」的问题。达梦数据库查看执行计划非常简单,用1个命令,3步就能看懂核心信息。
在SQL语句前加EXPLAIN关键字,执行后就会输出执行计划。比如查询订单表2024年的订单数据:
-- 查看订单查询的执行计划
EXPLAIN SELECT O.ORDER_ID,O.USER_ID
FROM ORDER_INFO O
WHERE O.ORDER_TIME BETWEEN '2024-01-01' AND '2024-12-31';
执行后会得到类似下面的结果,看起来像一串代码,其实按顺序解读就行(控制流从上到下,数据流从下到上):
1 #NSET2: (1, 500, 4000)
2 #PRJT2: (1, 500, 4000); exp_num(2), is_atom(FALSE)
3 #SLCT2: (1, 500, 4000); (O.ORDER_TIME >= '2024-01-01' AND O.ORDER_TIME <= '2024-12-31')
4 #CSCN2: (1, 500, 4000); INDEX33555476(ORDER_INFO as O); btr_scan(1)
新手不用逐字抠细节,重点看最下面的「表访问节点」(例子中是第4行),因为慢查询大多出在这里:
第4行(CSCN2):CSCN2是「聚集索引扫描」,后面跟着索引名INDEX33555476,说明走了索引扫描,这是高效的操作;
代价三元组(1, 500, 4000):成本1,处理500行数据,总字节4000,整体效率不错;
第3行(SLCT2):表示按时间条件过滤数据,对应SQL里的WHERE子句;
第2行(PRJT2):表示只取需要的ORDER_ID和USER_ID两列,对应SELECT子句;
第1行(NSET2):最终返回的结果集,把上面处理的结果汇总给用户。
光看懂还不够,关键是用执行计划定位问题。下面结合生产中最常见的4个痛点,教你一步步优化。
场景:订单表ORDER_INFO有100万数据,查询某时间段订单时,耗时超10秒,执行计划如下:
4 #CSCN2: (100, 1000000, 8000000); SYSINDEXORDER_INFO(ORDER_INFO as O); btr_scan(1)
问题定位:虽然操作符是CSCN2,但代价100、处理100万行,说明实际走了「全表扫描」(达梦中无合适索引时,聚集索引会退化为全表扫描)。原因是ORDER_TIME列没建索引,数据库只能逐行遍历所有数据。
优化方案:给过滤条件列建索引:
-- 给订单时间列建索引
CREATE INDEX idx_order_time ON ORDER_INFO(ORDER_TIME);
-- 重建执行计划后,再查询耗时降至300ms
场景:给ORDER_TIME建了索引,但查询还是慢,执行计划显示依然处理100万行。
问题定位:查看执行计划发现操作符是「SSCN2(二级索引全表扫描)」,而非「SSEK2(二级索引范围扫描)」。原因可能是统计信息过时,数据库不知道表数据量已经变大,优化器选错了执行路径;也可能是查询条件用了函数,比如WHERE DATE(ORDER_TIME) = '2024-01-01',索引失效。
优化方案:
更新统计信息,让优化器准确判断数据分布;
修改查询条件,避免函数操作索引列:WHERE ORDER_TIME BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59'。
场景:关联订单表和用户表查询时耗时超5秒,执行计划中有「BLKUP(回表查询)」操作符。
问题定位:BLKUP表示先通过二级索引找到ROWID,再回表取数据,频繁回表会增加IO消耗。比如用用户ID关联时,二级索引只存了用户ID和ROWID,需要多次回表取订单信息。
优化方案:建「覆盖索引」,把查询需要的列都包含在索引里,避免回表:
-- 覆盖索引:包含关联列和查询列
CREATE INDEX idx_user_order ON ORDER_INFO(USER_ID, ORDER_ID, ORDER_TIME);
-- 优化后执行计划中BLKUP消失,耗时降至500ms
场景:统计各用户订单数时,执行计划出现「HAGR2(HASH分组)」,耗时超3秒。
问题定位:HASH分组需要缓存所有数据再分组,大数据量下IO消耗大。原因是GROUP BY的列没有建索引,优化器无法使用更高效的「排序分组」。
优化方案:给GROUP BY列建索引,让优化器切换为排序分组:
-- 给用户ID列建索引(GROUP BY列)
CREATE INDEX idx_order_uid ON ORDER_INFO(USER_ID);
-- 优化后执行计划变为SAGR2(排序分组),耗时降至800ms
最后整理了生产中最常见的操作符,记不住可以收藏,遇到时对照看:
| 操作符 | 含义 | 效率 | 优化建议 |
|---|---|---|---|
| CSCN2 | 聚集索引扫描 | 高 | 正常使用,确保索引合适 |
| SSEK2/CSEK2 | 索引范围扫描 | 高 | 范围查询时优先出现,最优解 |
| SSCN2 | 二级索引全表扫描 | 低 | 检查查询条件,更新统计信息 |
| BLKUP | 回表查询 | 低 | 建覆盖索引,减少回表次数 |
| HAGR2 | HASH分组 | 中低 | 给GROUP BY列建索引,切换排序分组 |
| SAGR2 | 排序分组 | 中高 | 优先选择,需索引支持 |
| HASH JOIN | 哈希连接 | 中高 | 等值连接时高效,大数据量适用 |
| NEST LOOP | 嵌套连接 | 中 | 小表驱动大表时更高效 |
避免SELECT *:只查询需要的列,不仅减少数据传输,还能让覆盖索引发挥作用;
用COUNT(*)代替COUNT(列名):COUNT()直接读取索引行数,无需扫描数据,效率更高(注意:COUNT()包含NULL,COUNT(列名)不包含,需结合场景使用);
合理使用HINT提示:如果优化器选了差的执行计划,可手动指定索引或连接方式,比如SELECT /*+ INDEX(O, idx_order_time) */ ...。
遇到慢查询,先加EXPLAIN看执行计划;
找最下面的表访问节点,看操作符是不是高效的SSEK2/CSEK2/CSCN2;
若出现全表扫描、回表或HASH分组,优先建合适的索引(普通索引/覆盖索引/GROUP BY列索引),再更新统计信息。
执行计划看似复杂,但只要抓住「操作符」和「代价」两个核心,再结合实际场景练几次,就能快速搞定大部分生产慢查询问题。
文章
阅读量
获赞
