注册
达梦 SQL 执行计划基础操作符学习
专栏/技术分享/ 文章详情 /

达梦 SQL 执行计划基础操作符学习

歪比巴卜 2026/08/28 219 0 0
摘要

达梦 SQL 执行计划基础操作符学习

1 执行计划基础

1.1 什么是执行计划

SQL 是非过程化语言:开发者描述“要什么”,查询优化器决定“怎么做”。达梦的代价优化器(CBO)会根据表和索引结构、统计信息、谓词选择率、内存、IO 与 CPU 等因素,比较多条候选路径并选择估算代价较低的计划。

代价 COST 是优化器用于比较候选计划的估算值,并不等同于真实耗时。只有在同一数据库版本、相同参数、相同对象与统计信息条件下,代价比较才更有意义。

1.2 计划节点的三部分

示意:一条最简单的全表扫描计划

1   #NSET2: [1, 10000, 156]
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3       #CSCN2: [1, 10000, 156]; INDEX33556710(T1)
组成 示例 含义
操作符 CSCN2 数据库在该节点执行的动作
三元组 [1, 10000, 156] [代价,记录行数,字节数]
补充信息 INDEX…(T1) 表/索引、扫描范围、连接键、过滤条件等

第二个数字是优化器估算的节点输出行数,第三个数字是单行记录的估算字节数。二者共同决定中间结果的规模,进而影响连接、聚合、排序和内存选择。

1.3 执行顺序

文本计划是一棵树。缩进越深,节点越接近数据源,也越早执行;缩进相同,位于上方的分支先执行。可记为“最右最上先执行”。阅读时不要从第 1 行机械向下读,而要从最深层扫描节点开始向上回溯。

  1. 找到缩进最深的表或索引扫描节点。
  2. 观察扫描结果上方是否有 SLCT、BLKUP、SORT 等处理。
  3. 遇到 JOIN 时,分别读清左右输入、连接键与预计行数。
  4. 继续向上看聚合、去重、投影,最后由 NSET 返回结果集。

1.4 怎样查看预估与真实计划

方法 是否执行 SQL 适用场景 关键提醒
管理工具 F9 快捷键 快速查看预估计划 只看到优化器估算
EXPLAIN <SQL> 命令行检查 DML 计划 不会产生真实行数和耗时
EXPLAIN FOR 以结果集保存与比较计划 信息更丰富,可含索引建议
SET AUTOTRACE TRACE 查看真实行数与 IO 统计 SQL 会真实执行
ET / DBMS_SQLTUNE 看节点耗时、次数与占比 监控有额外开销,按版本启用

方法一:EXPLAIN

EXPLAIN SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB';

EXPLAIN 生成并打印估算计划,不真正执行目标 SQL,适合低风险地查看计划形态。

方法二:EXPLAIN FOR

EXPLAIN FOR SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB'; EXPLAIN AS PLAN_DEMO FOR SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB';

EXPLAIN FOR 以结果集返回更结构化的信息,包括计划层级、操作符、表、索引、扫描范围、预测行数、字节数、代价、过滤条件、连接条件、分区范围和建议等。指定计划名称后,便于后续对比。

方法三:DIsql AUTOTRACE

SET AUTOTRACE TRACE; SELECT COUNT(*) FROM SYSOBJECTS; SET AUTOTRACE OFF;

常用模式如下:

模式 是否执行 SQL 用途
OFF 关闭跟踪,常规执行
NL 打印计划中的 NEST LOOP 相关内容
INDEX / ON 打印表扫描方式、表名和索引
TRACE 打印实际执行采用的计划、结果集及部分运行统计
TRACEONLY TRACE 类似,但查询不打印结果集

TRACETRACEONLY 都会真正执行 SQL。两者还要求 ENABLE_MONITORMONITOR_SQL_EXECENABLE_MONITOR_DMSQL 均开启;

2. 怎样阅读计划树

示例计划:

1 #NSET2: [1, 12, 56]
2   #PRJT2: [1, 12, 56]; exp_num(2), is_atom(FALSE)
3     #NEST LOOP INDEX JOIN2: [1, 12, 56]
4       #CSCN2: [1, 4, 52]; INDEX...(T2 AS B)
5       #SSEK2: [1, 3, 4]; IDX_T1_C1(T1 AS A), scan_range[B.D1,B.D1]

2.1 两个方向

  • 控制流:从父节点向孩子节点传递,即从上向下请求数据。
  • 数据流:孩子节点产生数据后传给父节点,即从下向上返回数据。

入门阅读时,可以先找最深层叶子节点,确认数据从哪里取得;再沿缩进向上看数据如何连接、过滤、计算,最后由顶层输出。

同一深度的兄弟节点不能简单理解为“按行号同时执行”。它们的调用顺序由父操作符控制,例如 NEST LOOP 会反复驱动其中一个孩子访问另一个孩子。

flowchart LR
    subgraph C["控制流:父节点向下请求数据"]
        C1["NSET2 输出"] --> C2["PRJT2 计算查询项"] --> C3["JOIN 请求左右孩子"] --> C4["SCAN 读取数据"]
    end

    subgraph D["数据流:叶子节点向上返回数据"]
        D4["SCAN 产生行"] --> D3["JOIN 生成连接行"] --> D2["PRJT2 计算表达式"] --> D1["NSET2 返回客户端"]
    end

看计划时先“从下往上讲数据”,再“从上往下讲谁调用谁”。NEST LOOP 等操作符不是把两棵子树各执行一次,而是可能反复调用内侧子树。

2.2 三元组

计划节点后的:

[COST, ROW_NUMS, BYTES]

依次表示:

  1. 估算操作符代价。
  2. 估算处理或输出的记录行数。
  3. 估算每行记录的字节数。

最值得关注的是估算行数:它会影响访问路径、连接顺序和连接算法的选择。如果估算行数与实际行数差距很大,计划即使“看起来合理”也可能运行很慢。

3 数据访问类操作符

3.1 CSCN2:全表扫描

CSCN2 是 CLUSTER INDEX SCAN 的缩写,在达梦中表示通过聚集索引扫描整张表。当没有可用索引、过滤条件选择性较差,或者读取表中较大比例的数据时,优化器可能选择 CSCN2。

  • 合理场景:小表;报表需要读取大量行;索引回表成本高于顺序扫描。
  • 风险场景:大表只返回极少行;高并发频繁扫描;节点输出行数明显高于最终返回行数。
  • 检查方法:确认过滤列是否可索引、是否发生隐式类型转换、统计信息是否准确。

3.2 SSEK2 与 BLKUP2:二级索引扫描和回表

典型路径:先在二级索引定位,再回表取完整行

1   #NSET2: [0, 1, 156]
2     #PRJT2: [0, 1, 156]
3       #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [0, 1, 156]; scan_type(ASC),
            IDX_C1_T1(T1), scan_range[10,10]

SSEK2 先在二级索引中确定范围或定位行;如果 SELECT 列不完全包含在索引中,通常需要 BLKUP2 根据主键、聚集索引或 ROWID 获取表中其他列。

现象 可能原因 初步判断
SSEK2 后无 BLKUP2 索引已覆盖查询列 通常 IO 更少
SSEK2 后 BLKUP2 行数很少 索引过滤性好 通常适合 OLTP 精确查询
BLKUP2 行数巨大 索引过滤性差或估算错误 警惕随机 IO 与宽表取数
未走预期索引 类型不一致、函数包裹列、统计信息旧 先核对谓词和统计信息

3.3 CSEK2 与 SSCN

CSEK2 是聚集索引上的范围或定位扫描,数据行与聚集索引存储在一起,因此通常无需 BLKUP。SSCN 是二级索引全扫描:虽然扫描了完整索引,但如果索引较窄且覆盖查询列,仍可能比扫描整张宽表更便宜;同时索引顺序还可能帮助消除 SORT 或支持 SAGR。

4 行处理、聚合与排序

4.1 SLCT2、PRJT2、NSET2

操作符 做什么 诊断重点
SLCT2 过滤行,相当于关系代数中的选择 过滤前后行数;谓词是否可下推;估算与实际差异
PRJT2 计算 SELECT 列、表达式或函数 复杂函数、类型转换、输出行宽
NSET2 收集并返回最终结果集 通常是顶层包装,不要只盯根节点代价

SLCT2 是判断统计信息准确性的一个好入口:如果输入 100 万行、估算输出 2.5 万行,但实际只返回 10 行,说明选择率估算严重偏差,后续连接方式很可能也会被带偏。

4.2 AAGR2 与 FAGR2

AAGR2 用于没有 GROUP BY 的普通聚集,例如 COUNT、SUM、AVG、MAX、MIN。FAGR2 则用于某些无过滤条件的快速聚集场景,可直接从表或索引元信息/边界快速取得结果。

聚集示例

EXPLAIN SELECT COUNT(*) FROM T1 WHERE C1 = 10; -- 常见 AAGR2 EXPLAIN SELECT MAX(C1) FROM T1; -- 可能出现 FAGR2

4.3 HAGR2 与 SAGR2

对比项 HAGR2(HASH 分组) SAGR2(流式分组)
输入要求 无需预先有序 分组键需有序
主要资源 内存中的 HASH 表;不足可能刷盘 顺序处理,内存压力通常较小
常见来源 全表扫描后 GROUP BY 索引有序扫描后 GROUP BY
检查重点 分组数估算、内存和临时空间 有序性是否真实可用

不要把 SAGR2 一概视为更优:为了获得有序输入而扫描大量索引或回表,也可能比一次全表扫描加 HAGR2 更贵。应结合整条数据流比较。

4.4 SORT3、DISTINCT 与 UNION

  • SORT3 可能由 ORDER BY、窗口计算、MERGE JOIN 前置排序、部分 GROUP BY 或 DISTINCT 引入。
  • 重点看排序输入行数和行宽;中间结果越大,内存越容易不足并转为磁盘排序。
  • 合适的索引顺序有时可以消除 SORT,但索引扫描与回表本身也有代价。
  • UNION 需要去重,通常比 UNION ALL 多一层 HASH 或排序;业务允许重复时可优先考虑 UNION ALL。

5 连接类操作符

flowchart TB
    Q["两表连接"] --> EQ{"主要连接条件是否等值?"}
    EQ -->|"否:范围、不等值等"| NL["NEST LOOP"]
    EQ -->|"是"| SIZE{"驱动侧是否很小,内侧是否有高选择性索引?"}
    SIZE -->|"是"| NLI["NEST LOOP INDEX"]
    SIZE -->|"否,两侧数据较大"| HASH["HASH JOIN"]
    EQ -->|"两侧已按连接键有序"| MERGE["MERGE JOIN"]

    NL --> NR["风险:内侧被反复扫描"]
    NLI --> NIR["风险:大量索引定位和回表"]
    HASH --> HR["风险:内存、刷盘、数据倾斜"]
    MERGE --> MR["风险:额外 SORT3 代价"]

这是一张学习判断图,不是优化器的完整决策规则。实际选择还受统计信息、提示、并行、参数和版本实现影响。

5.1 NEST LOOP 与 NEST LOOP INDEX JOIN

嵌套循环以一侧作为驱动输入,对每一行去另一侧匹配。

适合 风险 检查问题
驱动侧过滤后很小 驱动侧实际行数远大于估算 最左/上输入到底输出多少行?
被驱动侧连接键有高选择性索引 每次探测都触发随机 IO 右侧是否出现 SSEK + BLKUP?
返回结果集较小 普通 NEST LOOP 两侧全扫 是否缺失连接条件或索引?

5.2 HASH JOIN

HASH JOIN 通常用于等值连接。优化器选择一侧构造 HASH 表,另一侧逐行计算哈希值并探测匹配。它不依赖被探测侧索引,适合大数据量连接,但依赖准确的行数估算和足够内存。

  • 构造侧宜较小,否则构建成本和内存占用上升。
  • 两侧通常需要扫描较多数据,因此最终只返回少量行时要检查过滤是否能提前。
  • 内存不足可能使用临时空间;ET/监控中应关注操作符耗时与临时空间迹象。
  • HASH 冲突过多会增加比较成本,统计信息和数据分布非常重要。

5.3 MERGE JOIN

MERGE JOIN 对两个按连接键有序的输入进行归并。它通常稳定、内存压力较可控,但必须获得有序输入;如果输入本身无序,就可能增加 SORT 成本。两个连接列已有合适索引时,更容易看到 MERGE JOIN。

5.4 SEMI JOIN 与 ANTI JOIN

半连接常用于 EXISTS 或 IN:只需要判断另一侧是否存在匹配行,找到一个匹配即可,不输出另一侧的全部列。反半连接常用于 NOT EXISTS 等“不存在”判断。它们可以由 HASH、NEST LOOP INDEX 或 MERGE 等物理算法实现。

SEMI / ANTI 语义示例

-- 半连接语义:返回 T2 中在 T1 存在匹配 ID 的行 SELECT * FROM T2 WHERE EXISTS (SELECT 1 FROM T1 WHERE T1.ID = T2.ID); -- 反半连接语义:返回 T2 中不存在匹配 ID 的行 SELECT * FROM T2 WHERE NOT EXISTS (SELECT 1 FROM T1 WHERE T1.ID = T2.ID);

6 两个执行计划拆解示例

6.1 示例一:索引扫描并回表

按“最右最上”顺序阅读

EXPLAIN SELECT * FROM T1 WHERE C1 = 10; 1 #NSET2: [0, 1, 156] 2 #PRJT2: [0, 1, 156] 3 #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1) 4 #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
  1. SSEK2 在 IDX_C1_T1 上定位 C1=10 的索引项。
  2. BLKUP2 根据索引定位信息回表取得 SELECT * 所需的其他列。
  3. PRJT2 计算并输出选择项。
  4. NSET2 收集并返回结果。

6.2 示例二:HASH 等值连接

示意计划:先过滤 T1,再与 T2 做 HASH 连接

SELECT * FROM T1 JOIN T2 ON T1.C1 = T2.C1 WHERE T1.C2 = 'A'; 1 #NSET2: [4, 24502, 296] 2 #PRJT2: [4, 24502, 296] 3 #HASH2 INNER JOIN: [4, 24502, 296]; KEY(T1.C1=T2.C1) 4 #SLCT2: [1, 250, 148]; T1.C2 = 'A' 5 #CSCN2: [1, 10000, 148]; T1 6 #CSCN2: [1, 10000, 148]; T2
  1. 扫描 T1,并由 SLCT2 过滤 C2=‘A’。
  2. 扫描 T2。
  3. HASH2 INNER JOIN 按 C1 做等值匹配。
  4. PRJT2 输出列,NSET2 返回结果。

7 ET 指标

指标 含义 怎么用
TIME 节点实际时间 优先定位真实耗时高的节点
PERCENT 占总时间比例 用于排序优化优先级
RANK 耗时名次 快速找到 Top 节点
SEQ 执行计划节点号 映射回文本计划
N_ENTER 进入/调用次数 识别循环探测与重复执行
逻辑读 缓冲区页访问量 返回少但逻辑读大时有优化空间
物理读 磁盘读取量 结合缓存状态判断 IO 瓶颈
sorts(disk) 发生磁盘排序 检查排序输入、索引和排序内存

8 入门练习

DROP TABLE IF EXISTS OP_EMP; CREATE TABLE OP_EMP ( ID INT PRIMARY KEY, DEPT_NO INT, STATUS VARCHAR(10), SALARY DECIMAL(12,2), NAME VARCHAR(50) ); INSERT INTO OP_EMP SELECT LEVEL, MOD(LEVEL,100), CASE WHEN MOD(LEVEL,20)=0 THEN 'INACTIVE' ELSE 'ACTIVE' END, 3000 + MOD(LEVEL,20000), 'EMP_' || LEVEL FROM DUAL CONNECT BY LEVEL <= 100000; COMMIT;

练习 1:全表扫描与索引定位。对 DEPT_NO=10 查询执行 EXPLAIN;创建 DEPT_NO 索引并收集统计信息后再比较 CSCN2 与 SSEK2/BLKUP2。

练习 2:覆盖索引与回表。分别查询 DEPT_NO 与 SELECT *,观察覆盖查询是否能减少或消除 BLKUP2。

练习 3:排序。执行 WHERE DEPT_NO=10 ORDER BY SALARY;比较普通索引与 (DEPT_NO,SALARY) 组合索引是否影响 SORT3。

练习 4:聚合。分别对无索引列和有序索引列 GROUP BY,观察 HAGR2 与 SAGR2。

练习 5:估算与实际。使用 AUTOTRACE/ET 比较 STATUS=‘INACTIVE’ 的估算行数与真实行数,思考直方图或统计信息的作用。

常见操作符速查表

类别 操作符 核心含义 初学者关注点
结果 NSET2 收集并返回结果集,通常位于顶层 一般不是首要优化点
表达式 PRJT2 投影:计算并输出 SELECT 列/表达式 关注复杂函数或宽行
过滤 SLCT2 选择:对输入行应用 WHERE/过滤条件 看过滤前后行数差距
聚合 AAGR2 无 GROUP BY 的普通聚集 COUNT/SUM/MAX/MIN 等
聚合 FAGR2 快速获得 MIN/MAX/COUNT 等 通常无需读取完整数据
聚合 HAGR2 基于 HASH 的分组聚集 关注内存、刷盘、估算行数
聚合 SAGR2 有序输入上的流式分组聚集 常借助索引顺序
扫描 CSCN2 通过聚集索引扫描全表 大表+低返回比例时重点检查
扫描 SSEK2 二级索引范围/定位扫描 常与 BLKUP2 配套
扫描 CSEK2 聚集索引范围/定位扫描 无需二次回表
扫描 SSCN 二级索引全扫描 可用于覆盖、排序或分组
取数 BLKUP2 根据索引定位信息回表取其他列 大量离散回表可能产生随机 IO
排序 SORT3 对输入行排序 看内存排序或磁盘排序
连接 NEST LOOP 逐行驱动另一输入 小驱动集+被驱动侧索引最合适
连接 HASH JOIN 构造 HASH 表后探测匹配 适合大数据等值连接,关注内存
连接 MERGE JOIN 对两个有序输入归并 稳定,但需有序输入
子查询 SEMI / ANTI 实现 EXISTS/IN 或 NOT EXISTS 等 只判断存在性,不输出另一侧列
集合 DISTINCT / UNION 去重或集合合并 若无需去重优先考虑 UNION ALL
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服