执行计划是 SQL 调优的"X 光片"。这篇文章从零开始,带你读懂达梦的执行计划,把 5 种扫描操作符的原理和优化手段一次讲透。扫描操作符是执行计划的最底层,直接决定访问数据的方式和速度——搞懂它,调优就有了根。
系统版本:
[root@db01:/root]#
[root@db01:/root]# cat /etc/*release*
...
CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"
CentOS Linux release 7.6.1810 (Core)
CentOS Linux release 7.6.1810 (Core)
cpe:/o:centos:centos:7
数据库版本:
[dmdba@db01:/home/dmdba]$ disql sysdba/XXXX
服务器[LOCALHOST:5236]:处于普通打开状态
登录使用时间 : 13.711(ms)
密钥过期时间:2027-04-17
disql V9
11:22:11 sysdba@LOCALHOST:5236 SQL> select * from v$instance\G
*************************** 1. row ***************************
NAME: ZSDMDB
INSTANCE_NAME: ZSDMDB
INSTANCE_NUMBER: 1
HOST_NAME: db01
SVR_VERSION: DM Database Server x64 V9
DB_VERSION: DB Version: 0x7000d
START_TIME: 2026-09-08 11:18:48
STATUS$: OPEN
MODE$: NORMAL
OGUID: 0
DSC_SEQNO: 0
DSC_ROLE: NULL
BUILD_VERSION: 03151060506-20260417-322930-20218
BUILD_TIME: May 13 2026 15:49:55
已用时间: 6.556(毫秒). 执行号:603.
11:26:59 sysdba@LOCALHOST:5236 SQL> SELECT TO_NUMBER(SUBSTR(VER, 1, 2), 'XX'),TO_NUMBER(SUBSTR(VER, 3, 2), 'XX'),
2 TO_NUMBER(SUBSTR(VER, 5, 2), 'XX'),
3 TO_NUMBER(SUBSTR(VER, 7, 2), 'XX')
4 FROM (SELECT RAWTOHEX(CAST(SUBSTR(VER, 3) AS INT)) AS VER
5 FROM (SELECT REGEXP_SUBSTR(ID_CODE, '[^-]+', 1, 1) AS VER));
TO_NUMBER(SUBSTR(VER,1,2),'XX') TO_NUMBER(SUBSTR(VER,3,2),'XX') TO_NUMBER(SUBSTR(VER,5,2),'XX') TO_NUMBER(SUBSTR(VER,7,2),'XX')
------------------------------- ------------------------------- ------------------------------- -------------------------------
9 1 0 26
已用时间: 21.899(毫秒). 执行号:604.
版本号解析出来是 9.1.0.26。
架构:单机。
-- ============================================================
-- 场景:物联网设备温度上报
-- ============================================================
conn zs/Zs123456
-- 1. 设备表:普通聚集表,用于演示 CSCN2 / SSEK2 / SSCN / BLKUP2
CREATE TABLE iot_device (
device_id INT,
device_name VARCHAR(50),
temp INT,
voltage INT,
region_id INT
);
INSERT INTO iot_device
SELECT level,
'device_' || level,
MOD(level, 60) + 10, -- 温度 10 ~ 69
220 + MOD(level, 20), -- 电压
MOD(level, 8) -- 区域 0 ~ 7
FROM DUAL
CONNECT BY level <= 12000;
CREATE INDEX idx_iot_device_temp ON iot_device(temp);
-- 2. 设备日志表:主键表,用于演示主键扫描、回表、范围扫描
CREATE TABLE iot_log (
log_id INT PRIMARY KEY,
device_id INT,
log_level VARCHAR(20),
message VARCHAR(100),
log_time DATETIME
);
INSERT INTO iot_log
SELECT level,
MOD(level, 2000) + 1,
CASE MOD(level, 5)
WHEN 0 THEN 'ERROR'
WHEN 1 THEN 'WARN'
WHEN 2 THEN 'INFO'
WHEN 3 THEN 'INFO'
ELSE 'DEBUG'
END,
'log message ' || level,
DATEADD(DAY, -MOD(level, 180), SYSDATE)
FROM DUAL
CONNECT BY level <= 15000;
CREATE INDEX idx_iot_log_time ON iot_log(log_time);
-- 3. 区域维表:小表,用于演示小表全扫描、连接场景
CREATE TABLE iot_region (
region_id INT PRIMARY KEY,
region_name VARCHAR(50),
city VARCHAR(50)
);
INSERT INTO iot_region
SELECT level,
'region_' || level,
CASE
WHEN level <= 3 THEN 'Beijing'
WHEN level <= 6 THEN 'Shanghai'
ELSE 'Shenzhen'
END
FROM DUAL
CONNECT BY level <= 10;
-- 4. 聚簇索引演示表:用于演示 CSEK2
CREATE TABLE iot_cluster_metric (
metric_id INT CLUSTER PRIMARY KEY,
metric_val VARCHAR(50),
device_id INT
);
INSERT INTO iot_cluster_metric
SELECT level,
'metric_value_' || level,
MOD(level, 500) + 1
FROM DUAL
CONNECT BY level <= 10000;
-- 5. 批量参数/数组绑定演示表:用于演示 PSCN 场景
-- 说明:PSCN 通常由 JDBC 批量绑定或 TABLE(...) 集合函数触发,
-- 这里建一张普通表,配合集合查询观察计划。
CREATE TABLE iot_batch_param (
batch_id INT,
batch_name VARCHAR(50),
batch_val INT
);
INSERT INTO iot_batch_param
SELECT level,
'batch_' || level,
MOD(level, 100)
FROM DUAL
CONNECT BY level <= 500;
-- 6. 收集统计信息(可选,但强烈建议)
CALL SP_TAB_STAT_INIT('ZS', 'IOT_DEVICE');
CALL SP_TAB_STAT_INIT('ZS', 'IOT_LOG');
CALL SP_TAB_STAT_INIT('ZS', 'IOT_REGION');
CALL SP_TAB_STAT_INIT('ZS', 'IOT_CLUSTER_METRIC');
CALL SP_TAB_STAT_INIT('ZS', 'IOT_BATCH_PARAM');
| 表名 | 行数 | 索引/主键 | 对应扫描操作符实验 |
|---|---|---|---|
iot_device |
12000 | idx_iot_device_temp(temp) |
CSCN2、SSEK2、SSCN、BLKUP2 |
iot_log |
15000 | 主键 log_id、idx_iot_log_time(log_time) |
主键扫描、范围扫描、BLKUP2 |
iot_region |
10 | 主键 region_id |
小表 CSCN2、连接驱动表 |
iot_cluster_metric |
10000 | CLUSTER PRIMARY KEY(metric_id) |
CSEK2 |
iot_batch_param |
500 | 无 | PSCN / 集合绑定场景 |
执行计划本质上是一棵操作符树。以下面这条查询为例:
10:17:05 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT device_id,device_name FROM iot_device WHERE temp > 25;
1 #NSET2: [1, 600, 76]
2 #PRJT2: [1, 600, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
3 #BLKUP2: [1, 600, 76]; IDX_IOT_DEVICE_TEMP(IOT_DEVICE); use_clu_addr(0)
4 #SSEK2: [1, 600, 76]; scan_type(ASC), IDX_IOT_DEVICE_TEMP(IOT_DEVICE), scan_range(25,max], is_global(0)
从最里层往外看,执行顺序是这样的:
SSEK2 → 用二级索引定位 temp > 25 的记录
BLKUP2 → 根据索引里的 rowid 回表,取出 device_id、device_name
PRJT2 → 投影出最终需要的列
NSET2 → 收集结果返回客户端
读法就一句话:
从里往外读,从下往上看。
最底层的 SSEK2 先跑,然后一层层往上,最后 NSET2 把结果丢给客户端。
SSEK2 —— 二级索引范围扫描#SSEK2: [1, 600, 76]; scan_type(ASC), IDX_IOT_DEVICE_TEMP(IOT_DEVICE), scan_range(25,max], is_global(0)
这是整棵树的数据入口。
| 字段 | 含义 |
|---|---|
SSEK2 |
二级索引精确定位 / 范围定位 |
[1, 600, 76] |
估算代价、估算行数、估算行宽 |
scan_type(ASC) |
按索引升序扫描 |
IDX_IOT_DEVICE_TEMP(IOT_DEVICE) |
用的是 iot_device 表上的 idx_iot_device_temp 索引 |
scan_range(25,max] |
扫描范围:temp > 25,左开右闭到最大值 |
is_global(0) |
非全局索引 |
在 DM9 里,SSEK2 并不只用于等值查询,它也能做索引上的范围扫描。scan_range(25,max] 就明确表示它是在索引 B+ 树上做范围定位。
BLKUP2 —— 回表#BLKUP2: [1, 600, 76]; IDX_IOT_DEVICE_TEMP(IOT_DEVICE); use_clu_addr(0)
| 字段 | 含义 |
|---|---|
BLKUP2 |
通过二级索引找到 rowid 后,回聚集表取完整行 |
IDX_IOT_DEVICE_TEMP(IOT_DEVICE) |
回表依据的索引 |
use_clu_addr(0) |
不使用聚簇地址,普通 rowid 回表 |
为什么需要回表?
因为 SQL 要查:
SELECT device_id, device_name
而索引 idx_iot_device_temp 只包含:
temp + rowid
它不包含 device_id 和 device_name。
所以 SSEK2 只能定位到"哪些行满足 temp > 25",但拿不到 device_id、device_name,必须再回聚集表去取。
这就是 BLKUP2 的作用。
PRJT2 —— 投影#PRJT2: [1, 600, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
| 字段 | 含义 |
|---|---|
PRJT2 |
投影操作符,只保留 SELECT 列表中的列 |
exp_num(6) |
表达式个数为 6 |
is_atom(FALSE) |
不是原子表达式 |
INFO_BITS(0) |
信息位,通常可忽略 |
SQL 只查两列:
SELECT device_id, device_name
但 exp_num(6) 显示有 6 个表达式,这通常是因为执行计划内部还包含了一些辅助表达式,比如 rowid、过滤条件、列引用等。真正返回给客户端的,仍然只是 device_id 和 device_name。
PRJT2 的作用就是列裁剪:把回表拿到的整行数据,裁成最终需要的列。
NSET2 —— 结果集收集#NSET2: [1, 600, 76]
| 字段 | 含义 |
|---|---|
NSET2 |
最外层操作符,收集结果并返回客户端 |
[1, 600, 76] |
估算代价、估算行数、估算行宽 |
它是整棵操作符树的根节点,负责把最终结果集返回给客户端。
[1, 600, 76] 怎么看每一行都有类似:
[1, 600, 76]
这三个数字通常表示:
| 位置 | 含义 |
|---|---|
| 第 1 个 | 估算代价 |
| 第 2 个 | 估算返回行数 |
| 第 3 个 | 估算行宽,单位字节 |
这里每一层都是 [1, 600, 76],说明优化器估算:
temp > 25 的行大约有 600 行;注意:600 是估算值,不是实际值。实际返回多少行,取决于 iot_device 表里 temp > 25 的真实数据分布。
一句话理解: 聚集表(达梦默认的表类型)的全表扫描,从头到尾把所有数据页读完。
触发场景
| 场景 | 说明 |
|---|---|
| 无 WHERE 条件 | 如 SELECT * FROM table |
| WHERE 列无索引 | 条件列上没索引可用 |
| 优化器主动选择 | 判断全表扫描比走索引更快(小表、大范围查询) |
场景一:必然走 CSCN2(无 WHERE)
10:43:25 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM iot_device;
1 #NSET2: [1, 12000, 76]
2 #PRJT2: [1, 12000, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
3 #CSCN2: [1, 12000, 76]; INDEX33555480(IOT_DEVICE); btr_scan(1); need_slct(0); prejudge_iescn(0)
场景二:无索引列上的查询(iot_device 表无 device_id 索引,必然全表扫描)
16:01:38 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM iot_device where DEVICE_NAME like 'aaaa%';
1 #NSET2: [1, 600, 76]
2 #PRJT2: [1, 600, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
3 #SLCT2: [1, 600, 76]; exp11 > 0; slct_pushdown(0)
4 #CSCN2: [1, 12000, 76]; INDEX33555480(IOT_DEVICE); btr_scan(1); need_slct(0); prejudge_iescn(0)
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter(exp11 > 0)
已用时间: 6.950(毫秒). 执行号:0.
优化策略
| 手段 | 效果 |
|---|---|
| 建二级索引(SSEK2) | 精确查找走索引,大范围仍走 CSCN2 |
| 建覆盖索引 | 走索引扫描 SSCN,无需回表 |
| MAX_SCAN_PAGES 调大 | CSCN2 每次 I/O 批量填充最多连续扫描页数,SSD 建议调大 |
| OLAP_FLAG=1 | OLAP 模式偏好 CSCN2(全表扫描) |
一句话理解: 扫描整个二级索引(B+ 树叶子节点),不碰聚集表的数据页。
触发场景
| 场景 | 说明 |
|---|---|
| 查询仅返回索引覆盖的列 | SSCN 代替 CSCN2,效率更高 |
| 无 WHERE 但只查索引列 | 优化器可能选索引全扫描 |
| 大范围索引扫描 | 范围条件覆盖索引大部分值时 |
示例:只查索引列(temp 在 IDX_IOT_DEVICE_TEMP 中),无 WHERE 条件时优化器可能选索引全扫描
16:02:18 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT temp FROM iot_device;
1 #NSET2: [1, 12000, 16]
2 #PRJT2: [1, 12000, 16]; exp_num(2), is_atom(FALSE); INFO_BITS(0)
3 #SSCN: [1, 12000, 16]; IDX_IOT_DEVICE_TEMP(IOT_DEVICE); btr_scan(1); is_global(0)
已用时间: 6.948(毫秒). 执行号:0.
与 CSCN2 的区别
| 对比项 | CSCN2 | SSCN |
|---|---|---|
| 扫描对象 | 聚集表(数据页) | 二级索引(索引页) |
| I/O 量 | 更大,读全部数据页 | 更小,只读索引页 |
| 适用场景 | 无索引、大范围 | 覆盖索引、只查索引列 |
优化策略
| 手段 | 效果 |
|---|---|
| 建覆盖索引 | 让 SSCN 覆盖更多查询列 |
| 加精确 WHERE 条件 | 缩小扫描范围,避免全索引扫描 |
| 统计信息及时更新 | 帮助优化器正确选择 SSCN 或 CSCN2 |
一句话理解: 通过二级索引的 B+ 树精确查找一行或多行,是最高效的访问方式之一。
触发场景
| 场景 | 说明 |
|---|---|
| 索引列等值查询 | WHERE temp = 25 |
| 复合索引全匹配 | WHERE a = ? AND b = ?,索引在 (a,b) 上 |
| 索引列范围查询 | 也走 SSEK2,但扫描范围更大 |
示例:索引列上的等值查询 → SSEK2,再回表取其他列
16:08:43 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM iot_device WHERE temp = 25;
1 #NSET2: [1, 300, 76]
2 #PRJT2: [1, 300, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
3 #BLKUP2: [1, 300, 76]; IDX_IOT_DEVICE_TEMP(IOT_DEVICE); use_clu_addr(0)
4 #SSEK2: [1, 300, 76]; scan_type(ASC), IDX_IOT_DEVICE_TEMP(IOT_DEVICE), scan_range[25,25], is_global(0)
已用时间: 4.252(毫秒). 执行号:0.
优化策略
| 手段 | 效果 |
|---|---|
| 建复合索引 | 多列等值查询可精确匹配 |
| 建覆盖索引 | 消除 BLKUP2,避免回表 |
| 避免索引列上做函数运算 | 否则索引失效,退化为 CSCN2 |
一句话理解: 通过聚簇索引(CLUSTER PRIMARY KEY)定位,数据即索引,无需回表。
触发场景
| 场景 | 说明 |
|---|---|
| 表有 CLUSTER PRIMARY KEY | 仅在此类表上出现 |
| 聚簇索引列等值查询 | WHERE metric_id = 100 |
| 聚簇索引列范围查询 | 同样走 CSEK2,范围定位 |
示例:聚簇索引等值查询,一次定位直接拿到所有数据
16:11:34 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM iot_cluster_metric WHERE metric_id = 100;
1 #NSET2: [1, 1, 68]
2 #PRJT2: [1, 1, 68]; exp_num(4), is_atom(FALSE); INFO_BITS(0)
3 #CSEK2: [1, 1, 68]; scan_type(UNIQUE), INDEX33555487(IOT_CLUSTER_METRIC), scan_range[100,100]
与 SSEK2 的区别
| 对比项 | SSEK2 | CSEK2 |
|---|---|---|
| 索引类型 | 二级索引 | 聚簇索引 |
| 是否回表 | 需要 BLKUP2 回表 | 无需回表,数据即索引 |
| 效率 | 高 | 极高 |
| 适用表 | 普通聚集表 | CLUSTER PRIMARY KEY 表 |
优化策略
| 手段 | 效果 |
|---|---|
| 热点查询列建聚簇索引 | 一次定位直接取数,省去回表 |
| 聚簇索引列保持稳定 | 避免频繁更新导致数据重排 |
| 范围查询控制范围 | 避免全聚簇索引扫描 |
一句话理解: 通过二级索引找到 rowid 后,再到聚集表中读取完整行数据。它是性能杀手,大量随机 I/O 都出在这。
触发场景
| 场景 | 说明 |
|---|---|
| 索引列定位后查所有列 | SELECT * 但索引不覆盖 |
| 查询列不在索引中 | 必须回聚集表取 |
| 大量行满足条件 | 每次回表都是一次随机 I/O |
示例:索引列定位后查所有列 → BLKUP2 回表
16:11:35 zs@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM iot_device WHERE temp = 25;
1 #NSET2: [1, 300, 76]
2 #PRJT2: [1, 300, 76]; exp_num(6), is_atom(FALSE); INFO_BITS(0)
3 #BLKUP2: [1, 300, 76]; IDX_IOT_DEVICE_TEMP(IOT_DEVICE); use_clu_addr(0)
4 #SSEK2: [1, 300, 76]; scan_type(ASC), IDX_IOT_DEVICE_TEMP(IOT_DEVICE), scan_range[25,25], is_global(0)
已用时间: 7.152(毫秒). 执行号:0.
优化策略
| 手段 | 效果 |
|---|---|
| 建覆盖索引 | SELECT 列都在索引中,直接消除 BLKUP2 |
| PLAN_OP_FLAG=12 | 让 BLKUP2 按 rowid 排序后批量回表,变随机 I/O 为顺序 I/O |
| BDTA_SIZE HINT | 加大批量回表缓冲区 |
| 操作符简称 | 全称 | 速度 | 何时出现 |
|---|---|---|---|
| CSCN2 | 聚集表全扫描 | 慢 | 无 WHERE、无索引、大范围 |
| SSCN | 索引全扫描 | 中等 | 有索引、只查索引列 |
| SSEK2 | 索引精确定位 | 很快 | 索引列等值/范围查询 |
| CSEK2 | 聚簇索引定位 | 很快 | CLUSTER PK 等值 |
| BLKUP2 | 回表 | 最慢 | SSEK2 后取非索引列 |
这张表不用背,看多了自然就有感觉。真正要记的是:越慢的操作符出现在越底层,问题就越大。
| 出现操作符 | 问题 | 处理方案 |
|---|---|---|
CSCN2(大表) |
全表扫描慢 | 建索引 → SSEK2 + BLKUP2 |
SSEK2 + BLKUP2(大量) |
回表太多 | 建覆盖索引 或 PLAN_OP_FLAG=12 |
SSCN(大范围) |
索引扫描范围大 | 加精确 WHERE 条件 |
BLKUP2(随机 I/O 高) |
随机回表 | PLAN_OP_FLAG=12 + BDTA_SIZE HINT |
调优不是玄学,就是看到什么操作符,对应做什么动作。这张表当个抓手用就行。
执行计划这棵树,得从下往上看。最底下那个操作符,基本就决定了这条 SQL 快不快。
五种扫描操作符,简要说明:
CSCN2 最费劲,全表从头扫到尾,大表上看见它就得留个心眼。
SSCN 只扫索引不碰数据页,但前提是查询的列刚好都在索引里。
SSEK2 B+ 树上一枪命中,等值查询基本靠它。
CSEK2 聚簇索引数据一体,定位就是取数,连回表都省了。
BLKUP2 本身不慢,但随机 I/O 一多,再好的索引也能被它拖垮。
说白了就两句话:先看最底层是什么扫描,再想办法把回表干掉。
文章
阅读量
获赞
