SQL 是非过程化语言:开发者描述“要什么”,查询优化器决定“怎么做”。达梦的代价优化器(CBO)会根据表和索引结构、统计信息、谓词选择率、内存、IO 与 CPU 等因素,比较多条候选路径并选择估算代价较低的计划。
代价 COST 是优化器用于比较候选计划的估算值,并不等同于真实耗时。只有在同一数据库版本、相同参数、相同对象与统计信息条件下,代价比较才更有意义。
示意:一条最简单的全表扫描计划
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 行机械向下读,而要从最深层扫描节点开始向上回溯。
| 方法 | 是否执行 SQL | 适用场景 | 关键提醒 |
|---|---|---|---|
| 管理工具 F9 快捷键 | 否 | 快速查看预估计划 | 只看到优化器估算 |
| EXPLAIN <SQL> | 否 | 命令行检查 DML 计划 | 不会产生真实行数和耗时 |
| EXPLAIN FOR | 否 | 以结果集保存与比较计划 | 信息更丰富,可含索引建议 |
| SET AUTOTRACE TRACE | 是 | 查看真实行数与 IO 统计 | SQL 会真实执行 |
| ET / DBMS_SQLTUNE | 是 | 看节点耗时、次数与占比 | 监控有额外开销,按版本启用 |
EXPLAIN SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB';
EXPLAIN 生成并打印估算计划,不真正执行目标 SQL,适合低风险地查看计划形态。
EXPLAIN FOR
SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB';
EXPLAIN AS PLAN_DEMO FOR
SELECT * FROM SYSOBJECTS WHERE SUBTYPE$ = 'STAB';
EXPLAIN FOR 以结果集返回更结构化的信息,包括计划层级、操作符、表、索引、扫描范围、预测行数、字节数、代价、过滤条件、连接条件、分区范围和建议等。指定计划名称后,便于后续对比。
SET AUTOTRACE TRACE;
SELECT COUNT(*) FROM SYSOBJECTS;
SET AUTOTRACE OFF;
常用模式如下:
| 模式 | 是否执行 SQL | 用途 |
|---|---|---|
OFF |
是 | 关闭跟踪,常规执行 |
NL |
否 | 打印计划中的 NEST LOOP 相关内容 |
INDEX / ON |
否 | 打印表扫描方式、表名和索引 |
TRACE |
是 | 打印实际执行采用的计划、结果集及部分运行统计 |
TRACEONLY |
是 | 与 TRACE 类似,但查询不打印结果集 |
TRACE 和 TRACEONLY 都会真正执行 SQL。两者还要求 ENABLE_MONITOR、MONITOR_SQL_EXEC、ENABLE_MONITOR_DMSQL 均开启;
示例计划:
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]
入门阅读时,可以先找最深层叶子节点,确认数据从哪里取得;再沿缩进向上看数据如何连接、过滤、计算,最后由顶层输出。
同一深度的兄弟节点不能简单理解为“按行号同时执行”。它们的调用顺序由父操作符控制,例如 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 等操作符不是把两棵子树各执行一次,而是可能反复调用内侧子树。
计划节点后的:
[COST, ROW_NUMS, BYTES]
依次表示:
最值得关注的是估算行数:它会影响访问路径、连接顺序和连接算法的选择。如果估算行数与实际行数差距很大,计划即使“看起来合理”也可能运行很慢。
CSCN2 是 CLUSTER INDEX SCAN 的缩写,在达梦中表示通过聚集索引扫描整张表。当没有可用索引、过滤条件选择性较差,或者读取表中较大比例的数据时,优化器可能选择 CSCN2。
典型路径:先在二级索引定位,再回表取完整行
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 与宽表取数 |
| 未走预期索引 | 类型不一致、函数包裹列、统计信息旧 | 先核对谓词和统计信息 |
CSEK2 是聚集索引上的范围或定位扫描,数据行与聚集索引存储在一起,因此通常无需 BLKUP。SSCN 是二级索引全扫描:虽然扫描了完整索引,但如果索引较窄且覆盖查询列,仍可能比扫描整张宽表更便宜;同时索引顺序还可能帮助消除 SORT 或支持 SAGR。
| 操作符 | 做什么 | 诊断重点 |
|---|---|---|
| SLCT2 | 过滤行,相当于关系代数中的选择 | 过滤前后行数;谓词是否可下推;估算与实际差异 |
| PRJT2 | 计算 SELECT 列、表达式或函数 | 复杂函数、类型转换、输出行宽 |
| NSET2 | 收集并返回最终结果集 | 通常是顶层包装,不要只盯根节点代价 |
SLCT2 是判断统计信息准确性的一个好入口:如果输入 100 万行、估算输出 2.5 万行,但实际只返回 10 行,说明选择率估算严重偏差,后续连接方式很可能也会被带偏。
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
| 对比项 | HAGR2(HASH 分组) | SAGR2(流式分组) |
|---|---|---|
| 输入要求 | 无需预先有序 | 分组键需有序 |
| 主要资源 | 内存中的 HASH 表;不足可能刷盘 | 顺序处理,内存压力通常较小 |
| 常见来源 | 全表扫描后 GROUP BY | 索引有序扫描后 GROUP BY |
| 检查重点 | 分组数估算、内存和临时空间 | 有序性是否真实可用 |
不要把 SAGR2 一概视为更优:为了获得有序输入而扫描大量索引或回表,也可能比一次全表扫描加 HAGR2 更贵。应结合整条数据流比较。
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 代价"]
这是一张学习判断图,不是优化器的完整决策规则。实际选择还受统计信息、提示、并行、参数和版本实现影响。
嵌套循环以一侧作为驱动输入,对每一行去另一侧匹配。
| 适合 | 风险 | 检查问题 |
|---|---|---|
| 驱动侧过滤后很小 | 驱动侧实际行数远大于估算 | 最左/上输入到底输出多少行? |
| 被驱动侧连接键有高选择性索引 | 每次探测都触发随机 IO | 右侧是否出现 SSEK + BLKUP? |
| 返回结果集较小 | 普通 NEST LOOP 两侧全扫 | 是否缺失连接条件或索引? |
HASH JOIN 通常用于等值连接。优化器选择一侧构造 HASH 表,另一侧逐行计算哈希值并探测匹配。它不依赖被探测侧索引,适合大数据量连接,但依赖准确的行数估算和足够内存。
MERGE JOIN 对两个按连接键有序的输入进行归并。它通常稳定、内存压力较可控,但必须获得有序输入;如果输入本身无序,就可能增加 SORT 成本。两个连接列已有合适索引时,更容易看到 MERGE 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);
按“最右最上”顺序阅读
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]
示意计划:先过滤 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
| 指标 | 含义 | 怎么用 |
|---|---|---|
| TIME | 节点实际时间 | 优先定位真实耗时高的节点 |
| PERCENT | 占总时间比例 | 用于排序优化优先级 |
| RANK | 耗时名次 | 快速找到 Top 节点 |
| SEQ | 执行计划节点号 | 映射回文本计划 |
| N_ENTER | 进入/调用次数 | 识别循环探测与重复执行 |
| 逻辑读 | 缓冲区页访问量 | 返回少但逻辑读大时有优化空间 |
| 物理读 | 磁盘读取量 | 结合缓存状态判断 IO 瓶颈 |
| sorts(disk) | 发生磁盘排序 | 检查排序输入、索引和排序内存 |
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 |
文章
阅读量
获赞
