注册
DM9 执行计划:5 种扫描操作符,看懂 SQL 快慢的底层逻辑【#达梦数据库 #达梦同行者征文】
专栏/技术分享/ 文章详情 /

DM9 执行计划:5 种扫描操作符,看懂 SQL 快慢的底层逻辑【#达梦数据库 #达梦同行者征文】

bicewow 2026/09/18 407 0 0
摘要

前言

执行计划是 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)

整体结构:一棵 4 层操作符树

从最里层往外看,执行顺序是这样的:

SSEK2  → 用二级索引定位 temp > 25 的记录
BLKUP2 → 根据索引里的 rowid 回表,取出 device_id、device_name
PRJT2  → 投影出最终需要的列
NSET2  → 收集结果返回客户端

读法就一句话:

从里往外读,从下往上看。

最底层的 SSEK2 先跑,然后一层层往上,最后 NSET2 把结果丢给客户端。

逐行拆解

第 4 行: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+ 树上做范围定位。

第 3 行: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 的作用。

第 2 行: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 的作用就是列裁剪:把回表拿到的整行数据,裁成最终需要的列。

第 1 行:NSET2 —— 结果集收集

#NSET2: [1, 600, 76]
字段 含义
NSET2 最外层操作符,收集结果并返回客户端
[1, 600, 76] 估算代价、估算行数、估算行宽

它是整棵操作符树的根节点,负责把最终结果集返回给客户端。

估算信息 [1, 600, 76] 怎么看

每一行都有类似:

[1, 600, 76]

这三个数字通常表示:

位置 含义
第 1 个 估算代价
第 2 个 估算返回行数
第 3 个 估算行宽,单位字节

这里每一层都是 [1, 600, 76],说明优化器估算:

  • 满足 temp > 25 的行大约有 600 行;
  • 每行大约 76 字节;
  • 代价为 1(这个代价看起来偏小,可能和统计信息、代价模型有关)。

注意:600 是估算值,不是实际值。实际返回多少行,取决于 iot_device 表里 temp > 25 的真实数据分布。

CSCN2 — 聚集表全表扫描

一句话理解: 聚集表(达梦默认的表类型)的全表扫描,从头到尾把所有数据页读完。

触发场景

场景 说明
无 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(全表扫描)

SSCN — 二级索引全扫描

一句话理解: 扫描整个二级索引(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

SSEK2 — 二级索引精确定位

一句话理解: 通过二级索引的 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

CSEK2 — 聚簇索引精确定位

一句话理解: 通过聚簇索引(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 表

优化策略

手段 效果
热点查询列建聚簇索引 一次定位直接取数,省去回表
聚簇索引列保持稳定 避免频繁更新导致数据重排
范围查询控制范围 避免全聚簇索引扫描

BLKUP2 — 回表操作

一句话理解: 通过二级索引找到 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 一多,再好的索引也能被它拖垮。

说白了就两句话:先看最底层是什么扫描,再想办法把回表干掉。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服