注册
SQL执行计划及常见操作符解读
专栏/技术分享/ 文章详情 /

SQL执行计划及常见操作符解读

,,, 2026/08/21 221 0 0
摘要

SQL执行计划

image.png

SQL 性能问题的核心通常集中在 执行计划(Execution Plan)。执行计划是优化器为某条 SQL 生成的“执行路线图”,决定了数据访问方式、连接顺序、聚合与排序策略等。

什么是执行计划

执行计划是数据库优化器基于 SQL 语句、表结构、索引与统计信息,进行成本估算后选择的一组执行步骤。它将 SQL 的执行过程拆成多个节点(操作符),每个节点表示一次表扫描、索引扫描、连接、聚合、排序或投影等操作。

执行计划和实际执行统计:

  • 估算计划(EXPLAIN)—— 不实际执行 SQL,返回优化器估算的执行步骤与代价。适合快速检查索引/连接方式。
  • 实际执行统计(ET ( Execution Trace)/ AUTOTRACE )—— 在真正执行 SQL 时收集的真实耗时与返回行数,能揭示估算与真实差距。

我们了解一下执行计划是怎么产生的

达梦处理一条SQL,大致会经历以下6个核心步骤:

阶段 核心任务 产出物
1. 词法分析 将SQL文本字符串,拆解成一个个有意义的词法单元(Token),如关键字、表名、操作符等。 Token流
2. 语法分析 根据SQL语法规则,将Token流组合成一棵语法树(Parse Tree),检查SQL语法是否正确。 语法树
3. 语义分析 对语法树进行语义检查,验证表、列是否存在,用户是否有权限等,并生成查询树(Query Tree) 查询树(关系树/逻辑计划)
4. 逻辑优化 基于关系代数规则,对查询树进行等价变换,如谓词下推、子查询展开、常量折叠等,不涉及数据分布。 逻辑优化后的查询树
5. 物理优化(CBO) 基于统计信息,为逻辑优化后的查询生成多种物理执行路径,并估算每个路径的代价,选择代价最低的执行计划 最优物理执行计划
6. 执行器执行 按照选中的执行计划,调用存储引擎接口,逐行或批量处理数据,并将结果返回给客户端。 查询结果集

经过分析和优化后,最后给到执行器的就是这条sql的执行计划

怎么查看执行计划

在disql中

EXPLAIN [SQL] SP_SET_PARA_VALUE(1,'ENABLE_MONITOR',1); #下面这个对性能影响较大,建议会话级开启 SP_SET_PARA_VALUE(1,'MONITOR_SQL_EXEC',1); #会话级开启 SF_SET_SESSION_PARA_VALUE('MONITOR_SQL_EXEC',1); 或 ALTER SESSION SET 'MONITOR_SQL_EXEC'=1 CALL ET(843); # 843为sql语句的执行号 #autotrace必须在disql中执行,上面两个监控必须开启 SET AUTOTRACE <OFF(缺省值) | NL | INDEX | ON | TRACE | TRACEONLY>
  • SET AUTOTRACE OFF 时,停止 AUTOTRACE 功能,常规执行语句。
  • SET AUTOTRACE NL 时,开启 AUTOTRACE 功能,不执行语句,如果执行计划中有嵌套循环操作,那么打印 NEST LOOP 相关操作符的内容。
  • SET AUTOTRACE INDEX(或者 ON)时,开启AUTOTRACE功能,不执行语句,如果有表扫描,那么打印执行计划中表扫描的方式、表名和索引。
  • SET AUTOTRACE TRACE 时,开启 AUTOTRACE 功能,执行语句,打印执行计划,并展示执行过程中的部分监控信息;需要设置 INI 中监控参数 ENABLE_MONITORMONITOR_SQL_EXECENABLE_MONITOR_DMSQL 均为开启(即等于1)才有实际意义。此功能与服务器 EXPLAIN 语句的区别在于,EXPLAIN 只生成执行计划,并不会真正执行SQL语句,因此产生的执行计划有可能不准;而通过 TRACE 获得的执行计划,是服务器实际执行的计划(可能是重用了计划缓存中计划,也可能是新生成的计划)。
  • SET AUTOTRACE TRACEONLY 时,开启AUTOTRACE功能,执行语句,打印执行计划,并展示执行过程中的部分监控信息;需要设置 INI 参数 ENABLE_MONITORMONITOR_SQL_EXECENABLE_MONITOR_DMSQL 均为开启(即等于1)才有实际意义。使用完毕后需要关闭。此功能与 TRACE 的区别在于对于查询语句不打印结果集。

ET执行结果含义

image.png

在DM图形化管理工具中

快捷键F9

image.png
image.png

点击执行号可查看ET

执行计划的执行顺序

首先,执行计划是由各类操作符组成的一颗树,也就是排序好的操作符的展现形式。

一般的执行计划格式为:

     OP1
         OP2
              OP3
              OP4
         OP5
              OP6
                  OP7
                  OP8

后序遍历即为执行计划顺序

OP3 -> OP4 -> OP2 -> OP7 -> OP8 -> OP6 -> OP5 -> OP1

image.png

实例解析

我们拟定一个执行计划与上例结构相同的 SQL:

CREATE TABLE TEST5(ID INT); CREATE TABLE TEST6(ID INT); CREATE TABLE TEST7(ID INT); CREATE TABLE TEST8(ID INT); INSERT INTO TEST5 VALUES(3); INSERT INTO TEST6 VALUES(4); INSERT INTO TEST7 SELECT LEVEL %100 FROM DUAL CONNECT BY LEVEL < 10000; INSERT INTO TEST8 SELECT LEVEL %100 FROM DUAL CONNECT BY LEVEL < 10000;

执行计划如下:

--SEL19
EXPLAIN SELECT /*+NO_USE_CVT_VAR*/* FROM
2   (
3   SELECT TEST5.ID FROM TEST5,TEST6 WHERE TEST5.ID = TEST6.ID
4   )
5   A,
6   (
7   SELECT ID FROM (SELECT TEST7.ID FROM TEST7,TEST8 WHERE TEST7.ID = TEST8.ID) GROUP BY ID
8   )
9   B
10  WHERE A.ID = B.ID;

1   #NSET2: [70, 1, 16]
2     #PRJT2: [70, 1, 16]; exp_num(2), is_atom(FALSE)
3       #HASH2 INNER JOIN: [70, 1, 16];  KEY_NUM(1);
4         #PRJT2: [0, 1, 8]; exp_num(1), is_atom(FALSE)
5           #HASH2 INNER JOIN: [0, 1, 8];  KEY_NUM(1);
6             #CSCN2: [0, 1, 4]; INDEX33555453(TEST5)
7             #CSCN2: [0, 1, 4]; INDEX33555454(TEST6)
8         #PRJT2: [68, 9801, 8]; exp_num(1), is_atom(FALSE)
9           #HAGR2: [68, 9801, 8]; grp_num(1), sfun_num(0);
10            #PRJT2: [4, 980100, 8]; exp_num(1), is_atom(FALSE)
11              #HASH2 INNER JOIN: [4, 980100, 8];  KEY_NUM(1);
12                #CSCN2: [1, 9999, 4]; INDEX33555455(TEST7)
13                #CSCN2: [1, 9999, 4]; INDEX33555456(TEST8)

这个例子(忽略 /*+no_use_cvt_var*/)的执行计划,我们暂时不关注 PRJT 和 NSET 操作符,只看 SQL 的执行顺序:

3       #HASH2 INNER JOIN: [70, 1, 16];  KEY_NUM(1);
5           #HASH2 INNER JOIN: [0, 1, 8];  KEY_NUM(1);
6             #CSCN2: [0, 1, 4]; INDEX33555453(TEST5)
7             #CSCN2: [0, 1, 4]; INDEX33555454(TEST6)
9           #HAGR2: [68, 9801, 8]; grp_num(1), sfun_num(0);
11              #HASH2 INNER JOIN: [4, 980100, 8];  KEY_NUM(1);
12                #CSCN2: [1, 9999, 4]; INDEX33555455(TEST7)
13                #CSCN2: [1, 9999, 4]; INDEX33555456(TEST8)

和前面的简单例子类似,执行顺序为:

6 -> 7 -> 5 -> 12 -> 13 -> 11 -> 9 -> 3

具体的执行过程就是:首先执行 TEST5 和 TEST6 的 HASH 连接,然后执行 TEST7 和 TEST8 的 HASH 连接并将连接结果进行 HASH 分组,最后将两个结果再次进行 HASH 连接得到最终结果集。

这个例子的 SQL 写法比较简单,意义也是非常明确的,读懂 SQL 之后把操作符顺序写下来不会很困难,同样的,只看到这个执行计划,我们需要能想出来这个 SQL 原本是什么样子的。

看的时候并不需要具体到每一步什么时候执行,只需要关注聚合节点做了什么以及他的两个子树(左表,右表)是哪些数据。

读懂 SQL 本身是关键,执行计划更多的是起一个提示作用,侧面告诉大家 SQL 需要做什么事情。

执行计划节点信息:代价三元组

能正确读取执行计划描述的执行顺序后,我们关注下执行计划各个节点的详细信息:执行计划中所有操作符的后面都会有一个三元组。

如:#CSCN2: [1, 9999, 4]

[1, 9999, 4] 就是我们提到的三元组,其中 3 个数字分别表示该操作符的估算代价、输出行数和涉及数据的行长

那么 #CSCN2: [1, 9999, 4] 表示的意义为:这是一个全表扫描操作,涉及的行数为 9999,每行数据长度为 4,整体代价估算为 1。

一般情况下,三元组中的首项和尾项仅有参考意义,我们只需要关注第二项,第二项的数值越少,表示这个操作符的代价也越小。

我们将三元组中的第二项称为估算行数(card),在复杂查询中,估算行数对于执行计划以及 SQL 性能的影响很大,而估算行数又受统计信息的影响,下一篇将会详细介绍统计信息及其对估算行数的影响。

操作符是什么

操作符是 SQL 执行的基本单元,所有的 SQL 语句最终都是转换成一连串的操作符最后在服务器上执行,得到需要的结果,因此了解操作符也是读懂执行计划的基础。

存储方式介绍

在介绍操作符之前,先介绍一些简单结构的存储方式,以便理解操作符的作用方式:

a. 一般普通表的物理存储CREATE TABLE T1(C1 INT,C2 INT)):

ROWID,C1,C2  ROWID,C1,C2  ROWID,C1,C2 …

这种方式进行存储,这里 ROWID 我们称之为聚簇 KEY,磁盘上面的数据按照 ROWID 顺序存储,每一行存储着表的完整数据。

b. 二级索引的存储CREATE INDEX I_TEST1 ON TABLE(C1)):

C1,ROWID  C1,ROWID  C1,ROWID  C1,ROWID …

可以看到,一般的二级索引不存储表的所有数据,仅按 C1 的顺序存储数据,但是附加存储了 C1 对应行的 ROWID,通过 ROWID,我们可以去基表上拿到整行的数据。

c. 聚簇索引的存储CREATE CLUSTER INDEX I_TEST2 ON TABLE(C1)):

C1,C2,ROWID  C1,C2,ROWID  C1,C2,ROWID …

这里同样是按 C1 的顺序存储数据,与二级索引不同的是保留了整行数据。

单表操作符

最基础的操作符是单行操作符,包括:

  • CSCN:基础全表扫描(a),从头到尾,全部扫描
  • SSCN:二级索引扫描(b),从头到尾,全部扫描
  • SSEK:二级索引范围扫描(b),通过键值精准定位到范围或者单值
  • CSEK:聚簇索引范围扫描(c),通过键值精准定位到范围或者单值
  • BLKUP:根据二级索引的 ROWID,回原表中取出全部数据(b + a)

3.1 CSCN 基础全表扫描

首先创建测试环境:创建表 T1 并录入数据,相关 SQL 语句如下:

CREATE TABLE T1(C1 INT,C2 INT); INSERT INTO T1 SELECT LEVEL,LEVEL FROM DUAL CONNECT BY LEVEL < 10000; COMMIT;

进行查询:

SELECT * FROM T1 WHERE C1 = 5;

接下来我们看看此次查询的执行计划:

--SEL1
EXPLAIN SELECT * FROM T1 WHERE C1 = 5;
1   #NSET2: [1, 249, 16]
2     #PRJT2: [1, 249, 16]; exp_num(3), is_atom(FALSE)
3       #SLCT2: [1, 249, 16]; T1.C1 = 5
4         #CSCN2: [1, 9999, 16]; INDEX33555446(T1)

在这个场景中我们创建了一个普通表,没有任何索引、过滤,那么从 T1 中取出数据只能走全表扫描 CSCN。

3.2 SSCN 二级索引扫描

下面我们在测试表中创建一条索引:

CREATE INDEX I_TEST1 ON T1(C1);

再看下面两条语句的执行计划:

--SEL2
EXPLAIN SELECT C1 FROM T1;

1   #NSET2: [1, 9999, 12]
2     #PRJT2: [1, 9999, 12]; exp_num(2), is_atom(FALSE)
3       #SSCN: [1, 9999, 12]; I_TEST1(T1)
--SEL3
EXPLAIN SELECT C2 FROM T1;

1   #NSET2: [1, 9999, 12]
2     #PRJT2: [1, 9999, 12]; exp_num(2), is_atom(FALSE)
3       #CSCN2: [1, 9999, 12]; INDEX33555446(T1)

创建索引之后 T1 存在两个入口,CSCN T1 基表和 SSCN 二级索引 I_TEST1。

  • 在 SEL2 中,只要求获取 C1,而二级索引上存在 C1,且数据长度比基础表 T1 要短(少一个 C2 的长度),因此查询走 SSCN 二级索引;
  • 对于 SEL3 来说,二级索引上没有 C2,因此要获取 C2 依然没有更好的入口,还是只能选择 CSCN 全表扫描。

一般来说,我们认为 CSCN 和 SSCN 的耗时是差不多的,二者的区别在于,SSCN 扫描出来的数据会按索引列排序,这一点在某些情况下是一大优势。

3.3 SSEK 二级索引范围扫描

--SEL4
EXPLAIN SELECT C1 FROM T1 WHERE C1 = 5;

1   #NSET2: [0, 249, 12]
2     #PRJT2: [0, 249, 12]; exp_num(2), is_atom(FALSE)
3       #SSEK2: [0, 249, 12]; scan_type(ASC), I_TEST1(T1), scan_range[5,5]

查询条件为"C1=XX",且存在 C1 索引(I_TEST1),所以走的 SSEK,需要注意的是操作符后面的描述 scan_range[5,5],表示精准定位到 5,多数情况下这样的查询是比较有效率的。

3.4 BLKUP 回表

根据二级索引的 ROWID 回原表中取出全部数据

对 SEL4 中的查询需求做一点修改:

--SEL5 EXPLAIN SELECT C1,C2 FROM T1 WHERE C1 = 5; 1 #NSET2: [0, 249, 16] 2 #PRJT2: [0, 249, 16]; exp_num(3), is_atom(FALSE) 3 #BLKUP2: [0, 249, 16]; I_TEST1(T1) 4 #SSEK2: [0, 249, 16]; scan_type(ASC), I_TEST1(T1), scan_range[5,5]

执行计划中出现 BLKUP 操作符,由于索引 I_TEST1 上没有 C2 的数据,而查询需要查出整行数据(SELECT *),因此索引需要执行 BLKUP 回原表查找整行数据。

回表是一个有优化空间的,例如上述例子,当我们在查询的列较少时,如上面的c1,c2时,我们可以考虑为c1和c2建立一个联合索引,这样子走联合索引的时候,索引中本身就包含了需要的所有数据,无需再进行回表扫描,在表列数和行数均较多的前提下,减少一次回表,意味着减少一次扫描和一大批无用数据的传递。

CREATE INDEX T1_C1_C2 ON T1(C1,C2); EXPLAIN SELECT C1,C2 FROM T1 WHERE C1 = 1; 1 #NSET2: [1, 25, 20] 2 #PRJT2: [1, 25, 20]; exp_num(3), is_atom(FALSE); INFO_BITS(0) 3 #SSEK2: [1, 25, 20]; scan_type(ASC), T1_C1_C2(T1), scan_range[(1,min),(1,max)), is_global(0)

3.5 CSEK 聚簇索引范围扫描

聚簇索引是比较特殊的索引(对应操作符 CSEK),在 DM 上,同一张表只允许存在一个聚簇索引,dm中,主键就是聚簇索引,没有主键的话,默认建表时,基表就是一个 ROWID 的聚簇索引,所以对 ROWID 的精准定位会走 CSEK:

--SEL6
EXPLAIN SELECT C1 FROM T1 WHERE ROWID = 6;

1   #NSET2: [0, 1, 12]
2     #PRJT2: [0, 1, 12]; exp_num(2), is_atom(FALSE)
3       #CSEK2: [0, 1, 12]; scan_type(ASC), INDEX33555446(T1), scan_range[exp_cast(6),exp_cast(6)]

如果我们在基表上再创建了一个自定义聚簇索引:

CREATE CLUSTER INDEX I_INDEX2 ON T1(C2);

那么 ROWID 这个聚簇索引就不存在了,取而代之的是按 C2 为顺序的聚簇索引:

--SEL7
EXPLAIN SELECT C1 FROM T1 WHERE ROWID = 6;

1   #NSET2: [1, 249, 12]
2     #PRJT2: [1, 249, 12]; exp_num(1), is_atom(FALSE)
3       #SLCT2: [1, 249, 12]; T1.ROWID = var1
4         #SSCN: [1, 9999, 12]; I_TEST1(T1)
--SEL8
EXPLAIN SELECT C1 FROM T1 WHERE C2 = 6;

1   #NSET2: [0, 249, 8]
2     #PRJT2: [0, 249, 8]; exp_num(1), is_atom(FALSE)
3       #CSEK2: [0, 249, 8]; scan_type(ASC), I_INDEX2(T1), scan_range[6,6]

对比 SEL6 和 SEL7,同样的查询语句,SEL6 走的 CSEK,而 SEL7 走的 SSCN,这是因为在 SEL7 中,已经没有 ROWID 的聚簇索引了,而普通二级索引 I_TEST1 上正好有查询需要的 C1 以及 ROWID,所以默认选择 SSCN 二级索引扫描 I_TEST1。同时我们可以看到,自定义 C2 的聚簇索引后,在 SEL8 中,对 C2 的精准过滤走的是 CSEK,且不存在 BLKUP。

单表的操作符大致就是这几种,这些是所有查询的数据来源,其他的复杂条件等都是在此基础上进行的操作。

过滤条件操作符 SLCT

过滤条件操作符比较简单,是对结果集进行过滤,需要注意的是这类操作符的描述信息,从描述信息中我们可以看到对于下层操作有哪些可用的过滤条件,这些过滤条件往往是优化方向的来源。

EXPLAIN SELECT * FROM T2 WHERE ID > 5 AND ID NOT LIKE '%C%';
-- 这里的id类型为varchar

1   #NSET2: [0, 1, 56]
2     #PRJT2: [0, 1, 56]; exp_num(2), is_atom(FALSE)
3       #SLCT2: [0, 1, 56]; (exp_cast(T2.ID) > 5 AND exp11 <= 0)
4         #CSCN2: [0, 8, 56]; INDEX33555447(T2)

需要关注的是 SLCT 的描述部分 (exp_cast(T2.ID) > 5 AND exp11 &lt;= 0),这里的描述部分将查询条件中的 ID > 5 标注为了 EXP_CAST(T2.ID) > 5,ID NOT LIKE ‘%c%’ 标注为了 EXP11 &lt;= 0

其中 EXP_CAST(T2.ID) > 5 提供的信息告诉我们,列 ID 和数字 5 进行比较时,是要对列作类型转换的,那么也就是说,就算 ID 列上存在索引,可能也不能进行范围扫描,因为索引范围扫描的输入要求是和索引列上的数据类型相同,

DM内部提供多种对比数据的方法以及转换类型的方法,但这些方法是没有覆盖所有类型的,不同类型数据进行比较时,会先选取一种用于比较的数据类型,再确定比较方法。

比如在此例中,比较 ID(VARCHAR) = 5(INT),服务器优先选择把类型转换成 INT 进行比较,导致 ID 列需要做类型转换,从而不能利用索引(索引存储的是转换之前的数据)。碰到这种情况,我们需要把索引列的对比对象(此例中的 INT)转换为和索引列一样的类型:

--SEL16
EXPLAIN SELECT * FROM T2 WHERE ID = '5';

1   #NSET2: [0, 1, 56]
2     #PRJT2: [0, 1, 56]; exp_num(2), is_atom(FALSE)
3       #SSEK2: [0, 1, 56]; scan_type(ASC), I_INDEX3(T2), scan_range['5','5']

将 ID(VARCHAR) = 5(INT) 中的 5(INT) 转换为 5(VARCHAR) 之后,就走索引进行查询了。

对于过滤条件操作符,我们希望做到的是:对于 SLCT 描述项中的每一个具体描述,都能在原始 SQL 查询语句中找到对应的条件,并思考是否存在优化的可能性。

多表关系处理

在实际应用过程中,查询时涉及到的往往不止一张表,不同的表之间存在一定的关系,处理多张表时就会涉及到多表操作符:

  • NEST LOOP (INNER LEFT RIGHT SEMI) 嵌套循环连接
  • HASH JOIN (INNER LEFT RIGHT SEMI) 哈希连接
  • INDEX JOIN (INNER LEFT RIGHT SEMI) 索引连接
  • MERGE JOIN 排序归并连接

4.1 NEST LOOP INNER JOIN

最基础的一种连接方式,将一张表的每一个值分别与另一张表的所有值拼接,形成一个大结果集,再从大结果集中过滤出满足条件的行。

SQL> EXPLAIN SELECT  FROM T1,T2 WHERE T1.ID = T2.ID;

1   #NSET2: [7, 20, 96]
2     #PRJT2: [7, 20, 96]; exp_num(2), is_atom(FALSE)
3       #SLCT2: [7, 20, 96]; T1.ID = T2.ID
4         #NEST LOOP INNER JOIN2: [7, 20, 96];
5           #CSCN2: [0, 5, 48]; INDEX33555457(T1)
6           #CSCN2: [0, 8, 48]; INDEX33555458(T2)

这里 T1 中存在 5 行数据,T2 中存在 8 行数据,NEST LOOP INNER JOIN 就是将这两个表无条件组成一张 5 × 8 = 40 行的表,然后对这 40 行的表依次筛选出 T1.ID = T2.ID 的数据(查询计划中的第 3 行 SLCT 操作符,后面章节会讲到)。

不难看出,这种方式是我们比较不希望看到的,如果 T1、T2 表较大,那么生成的基表会非常大,同样的,上层过滤条件需要执行的次数也非常多,这样效率非常低。

在输出上,这种连接方式的结果集按左表(T1)涉及的索引有序。

4.2 HASH JOIN

**没有索引的情况下,大多数连接的处理方式,是将一张表的连接列做成 HASH 表,**另一张表的数据向这个 HASH 表匹配,满足条件的值返回,计划的形式一般如下:

EXPLAIN SELECT * FROM T1,T2 WHERE T1.ID = T2.ID;

1   #NSET2: [0, 20, 96]
2     #PRJT2: [0, 20, 96]; exp_num(2), is_atom(FALSE)
3       #HASH2 INNER JOIN: [0, 20, 96];  KEY_NUM(1);
4         #CSCN2: [0, 5, 48]; INDEX33555457(T1)
5         #CSCN2: [0, 8, 48]; INDEX33555458(T2)

HASH连接分为两个阶段:

1)构造桶HASH表及节点链表(构建)

左表通过对连接列进行哈希操作形成一张哈希表,含key:连接列计算后的哈希值,values:原表某行数据的完整行或指针

2)数据扫描匹配(探测)

右表对连接列进行哈希计算后到左表进行匹配,匹配不到哈希值说明不存在,匹配到了哈希值再在values 中逐个匹配具体行,直到匹配或不存在。

可以看到,这样处理的计算量相比 NEST LOOP 会少很多,主要的计算量有三个部分:

  1. 对左右表的全表扫描(T1、T2);
  2. HASH 表的计算(取决于 HASH 算法的计算复杂度);
  3. 与右表(T2)每行数据进行匹配

由于所有的输出都是在扫描右表时完成的,所以 HASH JOIN 的输出是按右表(T2)涉及的索引有序的。

HASH连接的代价:

1)HASH表构造代价

2)左表扫描代价

3)右表扫描代价

4)HASH表探测代价

4.3 INDEX JOIN

将一张表(T1)的数据拿出,去另外一张表(T2)上进行范围扫描找出需要的数据行。索引连接需要右表的连接列上存在索引

CREATE INDEX I_TEST2 ON T2(ID);
11
EXPLAIN SELECT * FROM T1,T2 WHERE T1.ID = T2.ID;

1   #NSET2: [0, 17, 96]
2     #PRJT2: [0, 17, 96]; exp_num(2), is_atom(FALSE)
3       #NEST LOOP INDEX JOIN2: [0, 17, 96]
4         #CSCN2: [0, 5, 48]; INDEX33555457(T1)
5         #SSEK2: [0, 3, 0]; scan_type(ASC), I_TEST2(T2), scan_range[T1.ID,T1.ID]

这样的做法基本等价于:在右表(T2)上做 N 次 select * from t2 where id = ? 这样的语句,开销取决于该语句的结果集行数以及左表 T1 的行数,若两者都很小,那么这种方式是最理想的连接方式。

这种连接方式是按 T1 的基表操作符涉及的索引有序输出的。

4.4 MERGE JOIN

两张表都扫描索引,按照索引顺序进行归并。

CREATE INDEX I_INDEX2 ON T1(ID);
--SEL13
EXPLAIN SELECT /*+enable_index_join(0) enable_hash_join(0)*/* FROM T1,T2 WHERE T1.ID = T2.ID;
1   #NSET2: [0, 14, 96]
2     #PRJT2: [0, 14, 96]; exp_num(2), is_atom(FALSE)
3       #MERGE INNER JOIN3: [0, 14, 96];
4         #SSCN: [0, 5, 48]; I_INDEX2(T1)
5         #SSCN: [0, 8, 48]; I_TEST2(T2)

这里需要同时 SSCN 两条有序索引,将其中满足条件的值输出到结果集,效率比 NEST LOOP 要高很多,不考虑其他条件,如果 T1 和 T2 都很大的情况下跟 HASH JOIN 的效率相当(HASH JOIN 是 CSCN 两张基表,MERGE JOIN 则 SSCN 相关索引)。

这种连接的输出是按 T1 的索引严格有序的。

多表连接的操作符可以理解为复杂查询的基本单元,涉及到多表的复杂查询大多是由这些操作符组合而成,在此我们暂时只了解什么时候会出现这些不同的连接处理方式,具体的优化方式会在后面的章节中讲到。

4.5 SPL

当SQL包含复杂的子查询时,优化器可能选择将子查询的结果物化(Materialize)成一个内部的临时结果集(即SPL临时表),在执行计划中我们能看到类似SPL2 的操作符。

最主要、最典型的出现场景就是相关子查询,简单来说,当你的SQL语句中包含相关子查询时,优化器可能会选择将子查询的结果集先“物化”成一个内部的临时结果集(即SPL操作符),然后再与主查询进行关联操作

相关子查询是指子查询中引用了外部查询的列,导致子查询必须依赖外部查询的每一行数据来执行。如果外部查询返回大量数据,这种“逐行执行”的方式效率极低

我们使用示例库DMHR来演示

-- 查询员工信息,并附带显示其所在部门的名称 SELECT e.employee_id, e.employee_name, (SELECT d.department_name FROM departments d WHERE d.department_id = e.department_id) AS dept_name -- 这是一个相关子查询 FROM employees e;

执行计划如下

1   #NSET2: [1, 856, 68] 
2     #PIPE2: [1, 856, 68] 
3       #PRJT2: [1, 856, 68]; exp_num(4), is_atom(FALSE); INFO_BITS(0)
4         #CSCN2: [1, 856, 68]; INDEX33555489(EMPLOYEE as E); btr_scan(1); need_slct(0); prejudge_iescn(0)
5       #SPL2: [1, 1, 52]; key_num(1), spool_num(0), is_atom(TRUE), has_var(1), sites(-), result_cache(FALSE)
6         #PRJT2: [1, 1, 52]; exp_num(1), is_atom(TRUE); INFO_BITS(0)
7           #BLKUP2: [1, 1, 52]; INDEX33555485(D); use_clu_addr(0)
8             #SSEK2: [1, 1, 52]; scan_type(ASC), INDEX33555485(DEPARTMENT as D), scan_range[var1,var1], is_global(0)

我们分析一下这个执行计划

  1. #SSEK2 (第8行):这是对 DEPARTMENT 表进行的主键等值查询scan_range[var1,var1] 说明它使用主键索引(DEPARTMENT_PK)来查找 DEPARTMENT_ID = var1 的记录。这里的 var1 是一个变量,它的值由外层查询的当前行(EMPLOYEE 表)提供。
  2. #BLKUP2 (第7行):通过主键索引找到行地址后,回表去获取完整的行数据,即取出 DEPARTMENT_NAME 列。
  3. #PRJT2 (第6行):这是一个投影操作,可以理解为“只取需要的列”。结合逻辑,它只输出了 DEPARTMENT_NAME
  4. #SPL2 (第5行)key_num(1) 表明它有一个键(department_id),它将上述查询结果缓存起来。

因此,SPL2 内部存储的数据,逻辑上等价于执行了下面这个查询后得到的结果集(假设已经执行过):

SELECT DEPARTMENT_ID, DEPARTMENT_NAME FROM DEPARTMENT;

分组排序操作符

分组排序操作符:HAGRSAGR

分类排序操作符表示对取到的数据做归并或排序的处理,而归并和排序在某些情况下又是互通的。

我们先看这些操作符出现的基本情况:如果 SQL 语句中存在 GROUP,执行计划中大概率会出现这两个分组排序操作符之一(跳跃索引扫描之类的特殊情况暂不考虑)。

首先创建测试用表:

CREATE TABLE T3(ID INT,ID1 VARCHAR); INSERT INTO T3 SELECT LEVEL,LEVEL FROM DUAL CONNECT BY LEVEL < 10000;
--SEL17
EXPLAIN SELECT ID,SUM(ID1) FROM T3 GROUP BY ID;

1   #NSET2: [2, 99, 52]
2     #PRJT2: [2, 99, 52]; exp_num(2), is_atom(FALSE)
3       #HAGR2: [2, 99, 52]; grp_num(1), sfun_num(1);
4         #PRJT2: [1, 9999, 52]; exp_num(2), is_atom(FALSE)
5           #CSCN2: [1, 9999, 52]; INDEX33555450(T4)

HAGR 是最基础的分组方式,即进行 HASH AGR 操作,对于没有优化条件的分组语句,都会按这种方式进行分组,其分组原理和 HASH INNER JOIN 的方式类似:将原表数据取出,每个数据转换成 FOLD,发现有 FOLD 相同,且满足后续条件的数据合并为一组(类似 JOIN HASH SIZE,存在 INI 参数 HAGR_HASH_SIZE)。

不难发现,如果基表数据非常庞大,HAGR 的计算量是不容忽视的,那么在满足一定条件的情况下,我们可以利用有序性走 SAGR 操作符:

创建索引:

CREATE OR REPLACE INDEX I_TEST4 ON T4(ID,ID1);
--SEL18
EXPLAIN SELECT ID,SUM(ID1) FROM T4 GROUP BY ID;

1   #NSET2: [2, 99, 52]
2     #PRJT2: [2, 99, 52]; exp_num(2), is_atom(FALSE)
3       #SAGR2: [2, 99, 52]; grp_num(1), sfun_num(1)
4         #PRJT2: [1, 9999, 52]; exp_num(2), is_atom(FALSE)
5           #SSCN: [1, 9999, 52]; I_TEST4(T4)

这里出现了 SAGR 操作符,说明下层的输出是按分组列排序的,下层为 SSCN I_TEST4,而 I_TEST4 为 (ID,ID1) 组合索引,按照 ID 有序,满足 SAGR 条件。

SAGR 即 SORTED AGR 操作,不同于 HASH AGR,由于下层数据有序,同一分组的数据按照顺序取出即可,节省了大量的计算,但是下层数据量大时,开销依然需要注意。

大部分情况下如果 AGR 的下层输出在我们人为判断是有序的,但没有出现 SAGR 操作符,则可以判断计划存在问题。

流程控制符

达梦数据库中还有其他一些专注于流程控制的操作符。它们不直接处理数据,而是负责决定执行路径、协调子任务或管理数据的分发与收集

下面是一些典型的流程控制操作符及其作用:

操作符 简要说明
ACTRL 自适应计划控制。在执行过程中,根据实际运行情况,控制是否切换到备用执行计划。
PARALLEL 并行/分区控制。控制分区表的子分区裁剪或并行扫描。
GI 数据粒度控制。在达梦分布式集群(DPC)环境下,控制数据访问粒度和分区裁剪。
NSET2 结果集收集。通常是执行计划的最顶层节点,负责收集并整合所有子节点的结果。
ESEND / ERECV MPP数据分发/接收。在并行或分布式查询中,ESEND 负责将数据分发给下游节点,而 ERECV 负责接收数据。
ASSERT 约束检查。在执行DML操作时,负责检查数据是否违反了表上的约束条件。
PIPE2 流程控制。为左子节点的每一条数据去触发右子节点的执行(在上文的SPL中有提及)

所有执行计划操作符参考:

DM系统管理员手册-附录4-执行计划操作符

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服