如果把数据库查询比作一次导航,SQL 语句是“目的地”,执行计划就是数据库“导航系统”规划的详细路线。同一条 SQL 可能有多种执行方式,数据库的优化器会基于统计信息选择它认为成本最低的路线。EXPLAIN 命令的作用,就是将这条路线以可读的格式呈现出来,帮助开发者理解数据库是如何执行 SQL 的。
在达梦中,使用 EXPLAIN 命令查看 SQL 语句的执行计划,该命令不会实际执行 SQL,仅输出优化器估算的执行步骤与代价。
EXPLAIN <SQL语句>;
执行计划以树形结构呈现,阅读顺序为 从内到外(最缩进的操作符最先执行)。
TEST.EMPLOYEE:约 100004 行数据TEST.DEPT:10 行数据SQL 语句:
EXPLAIN SELECT ID, NAME FROM TEST.EMPLOYEE WHERE ID < 100;
执行计划输出:
1 #NSET2: [1, 91, 64]
2 #PRJT2: [1, 91, 64]; exp_num(3), is_atom(FALSE); INFO_BITS(0); BATCH_EXP_NUM(0); spl_info(NULL)
3 #BLKUP2: [1, 91, 64]; INDEX33555477(EMPLOYEE); use_clu_addr(0)
4 #SSEK2: [1, 91, 64]; scan_type(ASC), INDEX33555477(EMPLOYEE), scan_range(null2,100), is_global(0)
逐层解读:
| 层级 | 操作符 | 说明 |
|---|---|---|
| 4(最内层) | #SSEK2 |
对 ID 列的索引进行范围扫描,定位 ID < 100 的记录位置 |
| 3 | #BLKUP2 |
根据索引返回的 ROWID 回表读取完整行数据,获取 NAME 列 |
| 2 | #PRJT2 |
投影操作,仅保留 ID 和 NAME 两列 |
| 1 | #NSET2 |
结果集,返回最终数据 |
代价信息解析:[1, 91, 64] 分别表示:估算代价为 1、估算返回 91 行、估算数据量 64 字节。
首先为 NAME 列创建索引:
CREATE INDEX IDX_EMP_NAME ON TEST.EMPLOYEE(NAME);
SQL 语句:
EXPLAIN SELECT ID, NAME FROM TEST.EMPLOYEE WHERE NAME LIKE '张%';
执行计划输出:
1 #NSET2: [1, 1, 64]
2 #PRJT2: [1, 1, 64]; exp_num(3), is_atom(FALSE); INFO_BITS(0); BATCH_EXP_NUM(0); spl_info(NULL)
3 #BLKUP2: [1, 1, 64]; IDX_EMP_NAME(EMPLOYEE); use_clu_addr(0)
4 #SSEK2: [1, 1, 64]; scan_type(ASC), IDX_EMP_NAME(EMPLOYEE), scan_range['张','掌'), is_global(0)
关键差异:
scan_range['张','掌'):将 LIKE '张%' 转换为索引范围扫描(从 ‘张’ 到 ‘掌’ 的前开区间),这是达梦对字符串前缀匹配的优化实现方式。SQL 语句:
EXPLAIN SELECT E.NAME, D.DEPT_NAME
FROM TEST.EMPLOYEE E JOIN TEST.DEPT D ON E.DEPT = D.DEPT_ID;
执行计划输出:
1 #NSET2: [20, 333346, 148]
2 #PRJT2: [20, 333346, 148]; exp_num(2), is_atom(FALSE); INFO_BITS(0); BATCH_EXP_NUM(0); spl_info(NULL)
3 #HASH2 INNER JOIN: [20, 333346, 148]; KEY_NUM(1); KEY(D.DEPT_ID=exp_cast(E.DEPT)) KEY_NULL_EQU(0)
4 #CSCN2: [1, 10, 52]; INDEX33555485(DEPT as D); btr_scan(1); need_slct(0); prejudge_iescn(0)
5 #CSCN2: [12, 100004, 96]; INDEX33555476(EMPLOYEE as E); btr_scan(1); need_slct(0); prejudge_iescn(0)
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access(D.DEPT_ID = exp_cast(E.DEPT))
逐层解读:
| 层级 | 操作符 | 说明 |
|---|---|---|
| 5 | #CSCN2 |
全表扫描 TEST.EMPLOYEE(驱动表) |
| 4 | #CSCN2 |
全表扫描 TEST.DEPT(被驱动表) |
| 3 | #HASH2 INNER JOIN |
哈希内连接,连接条件为 D.DEPT_ID = exp_cast(E.DEPT) |
| 2 | #PRJT2 |
投影,选择 E.NAME 和 D.DEPT_NAME |
| 1 | #NSET2 |
结果集 |
Predicate Information:access(D.DEPT_ID = exp_cast(E.DEPT)) 显示连接条件,其中 exp_cast 表示 E.DEPT 发生了隐式数据类型转换,建议保持连接列的数据类型一致以避免影响索引使用。
SQL 语句:
EXPLAIN SELECT DEPT, COUNT(*) FROM TEST.EMPLOYEE GROUP BY DEPT;
执行计划输出:
1 #NSET2: [18, 2, 48]
2 #PRJT2: [18, 2, 48]; exp_num(2), is_atom(FALSE); INFO_BITS(0); BATCH_EXP_NUM(0); spl_info(NULL)
3 #HAGR2: [18, 2, 48]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(EMPLOYEE.DEPT) ; opt_info_bits(0)
4 #CSCN2: [11, 100004, 48]; INDEX33555476(EMPLOYEE); btr_scan(1); need_slct(0); prejudge_iescn(0)
逐层解读:
| 层级 | 操作符 | 说明 |
|---|---|---|
| 4 | #CSCN2 |
全表扫描 TEST.EMPLOYEE,读取所有行 |
| 3 | #HAGR2 |
哈希分组聚合,对 DEPT 列分组并计算 COUNT(*),grp_num(1) 表示 1 个分组键,sfun_num(1) 表示 1 个聚合函数 |
| 2 | #PRJT2 |
投影 |
| 1 | #NSET2 |
结果集,估算返回 2 行(表示 DEPT 只有 2 个不同值) |
| 操作符 | 说明 |
|---|---|
NSET2 |
结果集,执行计划最外层节点 |
PRJT2 |
投影操作,从结果中筛选指定列 |
SLCT2 |
过滤操作,对应 WHERE 条件 |
CSCN2 |
聚集索引全扫描(全表扫描) |
SSCN |
二级索引全扫描 |
SSEK2 |
二级索引范围扫描(精确定位) |
BLKUP2 |
根据二级索引的 ROWID 回表取完整行数据 |
HASH2 INNER JOIN |
哈希内连接,适用于大表等值连接 |
NEST LOOP JOIN2 |
嵌套循环连接,适用于小表驱动大表 |
AAGR2 |
简单聚集(无 GROUP BY 时的集函数计算) |
HAGR2 |
哈希分组聚集(包含 GROUP BY) |
SORT |
排序操作,对应 ORDER BY |
exp_cast |
隐式数据类型转换 |
[代价, 估算行数, 估算字节数],代价越小表示执行成本越低。CSCN2 出现于大表时,考虑添加索引。exp_cast 出现时,检查连接列的数据类型是否一致。| SQL 类型 | 操作符链 | 关键特征 |
|---|---|---|
| 等值条件查询(ID < 100) | SSEK2 → BLKUP2 → PRJT2 → NSET2 |
索引范围扫描 + 回表,估算 91 行 |
| 前缀匹配查询(LIKE ‘张%’) | SSEK2 → BLKUP2 → PRJT2 → NSET2 |
索引范围扫描,scan_range['张','掌') |
| 两表连接(JOIN) | CSCN2 → CSCN2 → HASH2 INNER JOIN → PRJT2 → NSET2 |
全表扫描 + 哈希连接,存在隐式类型转换 |
| 分组聚合(GROUP BY) | CSCN2 → HAGR2 → PRJT2 → NSET2 |
全表扫描 + 哈希分组,估算 2 个分组 |
通过 EXPLAIN 输出的执行计划,可以准确判断 SQL 的执行路径,从而识别性能瓶颈并进行针对性优化。
文章
阅读量
获赞
