SQL 性能问题的核心通常集中在 执行计划(Execution Plan)。执行计划是优化器为某条 SQL 生成的“执行路线图”,决定了数据访问方式、连接顺序、聚合与排序策略等。
执行计划是数据库优化器基于 SQL 语句、表结构、索引与统计信息,进行成本估算后选择的一组执行步骤。它将 SQL 的执行过程拆成多个节点(操作符),每个节点表示一次表扫描、索引扫描、连接、聚合、排序或投影等操作。
执行计划和实际执行统计:
我们了解一下执行计划是怎么产生的
达梦处理一条SQL,大致会经历以下6个核心步骤:
| 阶段 | 核心任务 | 产出物 |
|---|---|---|
| 1. 词法分析 | 将SQL文本字符串,拆解成一个个有意义的词法单元(Token),如关键字、表名、操作符等。 | Token流 |
| 2. 语法分析 | 根据SQL语法规则,将Token流组合成一棵语法树(Parse Tree),检查SQL语法是否正确。 | 语法树 |
| 3. 语义分析 | 对语法树进行语义检查,验证表、列是否存在,用户是否有权限等,并生成查询树(Query Tree)。 | 查询树(关系树/逻辑计划) |
| 4. 逻辑优化 | 基于关系代数规则,对查询树进行等价变换,如谓词下推、子查询展开、常量折叠等,不涉及数据分布。 | 逻辑优化后的查询树 |
| 5. 物理优化(CBO) | 基于统计信息,为逻辑优化后的查询生成多种物理执行路径,并估算每个路径的代价,选择代价最低的执行计划。 | 最优物理执行计划 |
| 6. 执行器执行 | 按照选中的执行计划,调用存储引擎接口,逐行或批量处理数据,并将结果返回给客户端。 | 查询结果集 |
经过分析和优化后,最后给到执行器的就是这条sql的执行计划
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_MONITOR、MONITOR_SQL_EXEC、ENABLE_MONITOR_DMSQL 均为开启(即等于1)才有实际意义。此功能与服务器 EXPLAIN 语句的区别在于,EXPLAIN 只生成执行计划,并不会真正执行SQL语句,因此产生的执行计划有可能不准;而通过 TRACE 获得的执行计划,是服务器实际执行的计划(可能是重用了计划缓存中计划,也可能是新生成的计划)。SET AUTOTRACE TRACEONLY 时,开启AUTOTRACE功能,执行语句,打印执行计划,并展示执行过程中的部分监控信息;需要设置 INI 参数 ENABLE_MONITOR、MONITOR_SQL_EXEC、ENABLE_MONITOR_DMSQL 均为开启(即等于1)才有实际意义。使用完毕后需要关闭。此功能与 TRACE 的区别在于对于查询语句不打印结果集。ET执行结果含义
快捷键F9
点击执行号可查看ET
首先,执行计划是由各类操作符组成的一颗树,也就是排序好的操作符的展现形式。
一般的执行计划格式为:
OP1
OP2
OP3
OP4
OP5
OP6
OP7
OP8
后序遍历即为执行计划顺序
OP3 -> OP4 -> OP2 -> OP7 -> OP8 -> OP6 -> OP5 -> OP1
我们拟定一个执行计划与上例结构相同的 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 的顺序存储数据,与二级索引不同的是保留了整行数据。
最基础的操作符是单行操作符,包括:
首先创建测试环境:创建表 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。
下面我们在测试表中创建一条索引:
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。
一般来说,我们认为 CSCN 和 SSCN 的耗时是差不多的,二者的区别在于,SSCN 扫描出来的数据会按索引列排序,这一点在某些情况下是一大优势。
--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,多数情况下这样的查询是比较有效率的。
根据二级索引的 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)
聚簇索引是比较特殊的索引(对应操作符 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。
单表的操作符大致就是这几种,这些是所有查询的数据来源,其他的复杂条件等都是在此基础上进行的操作。
过滤条件操作符比较简单,是对结果集进行过滤,需要注意的是这类操作符的描述信息,从描述信息中我们可以看到对于下层操作有哪些可用的过滤条件,这些过滤条件往往是优化方向的来源。
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 <= 0),这里的描述部分将查询条件中的 ID > 5 标注为了 EXP_CAST(T2.ID) > 5,ID NOT LIKE ‘%c%’ 标注为了 EXP11 <= 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 查询语句中找到对应的条件,并思考是否存在优化的可能性。
在实际应用过程中,查询时涉及到的往往不止一张表,不同的表之间存在一定的关系,处理多张表时就会涉及到多表操作符:
最基础的一种连接方式,将一张表的每一个值分别与另一张表的所有值拼接,形成一个大结果集,再从大结果集中过滤出满足条件的行。
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)涉及的索引有序。
**没有索引的情况下,大多数连接的处理方式,是将一张表的连接列做成 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 会少很多,主要的计算量有三个部分:
由于所有的输出都是在扫描右表时完成的,所以 HASH JOIN 的输出是按右表(T2)涉及的索引有序的。
HASH连接的代价:
1)HASH表构造代价
2)左表扫描代价
3)右表扫描代价
4)HASH表探测代价
将一张表(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 的基表操作符涉及的索引有序输出的。
两张表都扫描索引,按照索引顺序进行归并。
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 的索引严格有序的。
多表连接的操作符可以理解为复杂查询的基本单元,涉及到多表的复杂查询大多是由这些操作符组合而成,在此我们暂时只了解什么时候会出现这些不同的连接处理方式,具体的优化方式会在后面的章节中讲到。
当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)
我们分析一下这个执行计划
#SSEK2 (第8行):这是对 DEPARTMENT 表进行的主键等值查询。scan_range[var1,var1] 说明它使用主键索引(DEPARTMENT_PK)来查找 DEPARTMENT_ID = var1 的记录。这里的 var1 是一个变量,它的值由外层查询的当前行(EMPLOYEE 表)提供。#BLKUP2 (第7行):通过主键索引找到行地址后,回表去获取完整的行数据,即取出 DEPARTMENT_NAME 列。#PRJT2 (第6行):这是一个投影操作,可以理解为“只取需要的列”。结合逻辑,它只输出了 DEPARTMENT_NAME。#SPL2 (第5行):key_num(1) 表明它有一个键(department_id),它将上述查询结果缓存起来。因此,SPL2 内部存储的数据,逻辑上等价于执行了下面这个查询后得到的结果集(假设已经执行过):
SELECT DEPARTMENT_ID, DEPARTMENT_NAME FROM DEPARTMENT;
分组排序操作符:HAGR、SAGR。
分类排序操作符表示对取到的数据做归并或排序的处理,而归并和排序在某些情况下又是互通的。
我们先看这些操作符出现的基本情况:如果 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中有提及) |
所有执行计划操作符参考:
文章
阅读量
获赞
