likes
comments
collection
share

达梦数据库执行计划查看

作者站长头像
站长
· 阅读数 3

概述

数据库查询中,难免有SQL语句执行时间不满足预期,这个时候需要查看SQL执行计划,以便对SQL进行优化。下面将介绍达梦数据库如何查看执行计划及涉及相关工具。

1.执行计划查看顺序和说明

1.1 执行计划查看顺序

执行计划就是一条 SQL 语句在数据库中的执行过程或访问路径的描述。在遇到数据库性能问题时,往往需要查看慢SQL执行计划,下面就是一个例子:

达梦数据库执行计划查看

缩进越多的越先执行,同样缩进的上面的先执行, 下面的后执行,上下的优先级高于内外。 缩进最深的,最先执行; 缩进深度相同的,先上后下。 口诀:最右最上先执行。

1.2 执行计划查看说明

以上面1.1执行计划输出内容为例,执行计划的每行即为一个计划节点,主要包含三部分信息。

  • 第1部分  NEST2、PRJT2、CSCN2 为操作符及数据库具体执行了什么操作。
  • 第2部分  三元组为该计划节点的执行代价,具体含义为[代价,记录行数,字节数]。
  • 第3部分  为操作符的补充信息。
#CSCN2: [1, 856, 285]; INDEX33555892(EMPLOYEE)
 操作符,[代价,行数,字节数] 描述

以上面一行输出信息为例进行说明,操作符进行CSCN2了全表扫描,估算代价是1,行数是856行,涉及285个字节。

2.通过什么工具查看执行计划

2.1 通过 DM 数据库管理工具查看

如下图所示,选择查询语句后,点击按钮【显示执行计划P】或者执行【F9】即可查看SQL语句执行计划。

2.2 通过 explain 命令查看 

通过disql 登录到数据库中,使用explain命令查看执行计划,如下图所示:

达梦数据库执行计划查看

 3. 操作符说明

达梦中执行计划涉及到的一些主要操作符有如下表格,因执行计划操作都是英文缩写,为更好的理解操作符操作缩写含义,专门对其缩写前的英文描述进行整理。

达梦数据库执行计划查看

接下来结合实例来介绍下这些操作符:

准备测试数据:


DROP TABLE T1;
DROP TABLE T2;
CREATE TABLE T1(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) );
CREATE TABLE T2(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) );
INSERT INTO T1
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL FROM DUAL
CONNECT BY LEVEL<=10000;
INSERT INTO T2
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL  FROM DUAL
CONNECT BY LEVEL<=10000;
CREATE INDEX IDX_C1_T1 ON T1(C1);
SP_INDEX_STAT_INIT(USER,'IDX_C1_T1');

3.1 NSET收集结果集

SQL> EXPLAIN SELECT * FROM T1;
 
1   #NSET2: [1, 10000, 156] 
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE) 
3       #CSCN2: [1, 10000, 156]; INDEX33555483(T1)
 
已用时间: 0.458(毫秒). 执行号:0.

NSET 是用于结果集收集的操作符,一般是查询计划的顶层节点,优化中无需对该操作符过多关注。

3.2 PRJT投影

SQL> EXPLAIN SELECT * FROM T1;
 
1   #NSET2: [1, 10000, 156] 
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE) 
3       #CSCN2: [1, 10000, 156]; INDEX33555483(T1)
 
已用时间: 0.544(毫秒). 执行号:0.

投影(project)运算,就是选择查询的列,用于选择表达式项的计算;广泛用于查询,排序,函数索引创建等。

3.3 SLCT选择


SQL>  EXPLAIN SELECT * FROM T1 WHERE C2='TEST';
 
1   #NSET2: [1, 250, 156] 
2     #PRJT2: [1, 250, 156]; exp_num(5), is_atom(FALSE) 
3       #SLCT2: [1, 250, 156]; T1.C2 = 'TEST'
4         #CSCN2: [1, 10000, 156]; INDEX33555483(T1)
 
已用时间: 0.634(毫秒). 执行号:0.

SLCT操作符,用于查询条件的过滤,如果代价比较高,要考虑是否在查询条件相关列添加索引,及是分析是否有合适的索引。

3.4 CSCN全表扫描

SQL> EXPLAIN SELECT * FROM T1;
 
1   #NSET2: [1, 10000, 156] 
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE) 
3       #CSCN2: [1, 10000, 156]; INDEX33555483(T1)
 
已用时间: 0.465(毫秒). 执行号:0.
SQL> 

CSCN操作符是 CLUSTER INDEX SCAN 的缩写,通过聚集索引扫描全表,全表扫描 I/O 开销较大,如果没有选择谓词,或者没有索引可以利用,则一般只能做全表扫描。表数据量大的情况,应该避免通过添加有索引的查询条件。

3.5 BLKUP二次扫描

SQL> EXPLAIN SELECT * FROM T1 WHERE C1=10;
 
1   #NSET2: [1, 1, 156] 
2     #PRJT2: [1, 1, 156]; exp_num(5), is_atom(FALSE) 
3       #BLKUP2: [1, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [1, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
 
已用时间: 0.757(毫秒). 执行号:0.

3.6 SSEK二级索引扫


SQL> EXPLAIN SELECT * FROM T1 WHERE C1=10;
 
1   #NSET2: [1, 1, 156] 
2     #PRJT2: [1, 1, 156]; exp_num(5), is_atom(FALSE) 
3       #BLKUP2: [1, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [1, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
 
已用时间: 0.768(毫秒). 执行号:0.

SSEK2是二级索引扫描即先扫描索引,再通过主键、聚集索引、rowid 等信息去扫描表。

3.7 SSCN索引全扫描

SQL> CREATE INDEX IDX_C1_C2_T1 ON T1(C1,C2);
操作已执行
已用时间: 21.082(毫秒). 执行号:9101.
SQL> EXPLAIN SELECT C1,C2 FROM T1;
 
1   #NSET2: [1, 10000, 60] 
2     #PRJT2: [1, 10000, 60]; exp_num(3), is_atom(FALSE) 
3       #SSCN: [1, 10000, 60]; IDX_C1_C2_T1(T1)
 
已用时间: 0.656(毫秒). 执行号:0.

SSCN 是索引全扫描,不需要扫描表。

3.8 CSEK聚集索引扫描

SQL> CREATE CLUSTER INDEX IDX_C1_T2 ON T2(C1);
操作已执行
已用时间: 105.336(毫秒). 执行号:9102.
SQL> EXPLAIN SELECT * FROM T2 WHERE C1=10;
 
1   #NSET2: [1, 250, 156] 
2     #PRJT2: [1, 250, 156]; exp_num(5), is_atom(FALSE) 
3       #CSEK2: [1, 250, 156]; scan_type(ASC), IDX_C1_T2(T2), scan_range[10,10]
 
已用时间: 0.728(毫秒). 执行号:0.
SQL>

3.9 NEST LOOP嵌套循环连接

SQL>  explain select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
 
1   #NSET2: [17862, 24950, 296] 
2     #PRJT2: [17862, 24950, 296]; exp_num(8), is_atom(FALSE) 
3       #SLCT2: [17862, 24950, 296]; T1.C1 = T2.C1
4         #NEST LOOP INNER JOIN2: [17862, 24950, 296]; 
5           #SLCT2: [1, 250, 148]; T1.C2 = 'A'
6             #CSCN2: [1, 10000, 148]; INDEX33555483(T1)
7           #CSCN2: [1, 10000, 148]; IDX_C1_T2(T2)
 
已用时间: 0.787(毫秒). 执行号:0.
SQL>

原理: 两层嵌套循环结构,有驱动表和被驱动表之分。 选定一张表作为驱动表,遍历驱动表中的每一行,根据连接条件去匹配第二 张表中的行。驱动表的行数就是循环的次数,这个很大程度影响了执行效率。

需注意的问题: 选择小表作为驱动表。统计信息尽量准确,保证优化器选对驱动表。 大量的随机读。如果没有索引,随机读很致命,每次循环只能读一块, 不能读多块。使用索引可以解决这个问题。

连接列是否有索引,都可以走 NEST LOOP,但没有索引,执行效率会很差,可针对 T1 和 T2 的连接列创建索引,并收集统计信息,语句如下所示:


CREATE INDEX IDX_T1_C2  ON T1(C2);
CREATE INDEX IDX_T2_C1  ON T2(C1);
DBMS_STATS.GATHER_INDEX_STATS(USER,'IDX_T1_C2');
DBMS_STATS.GATHER_INDEX_STATS(USER,'IDX_T2_C1');

再次查看执行计划,执行成本明显下降:


SQL> explain select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
 
1   #NSET2: [9805, 17465, 296] 
2     #PRJT2: [9805, 17465, 296]; exp_num(8), is_atom(FALSE) 
3       #SLCT2: [9805, 17465, 296]; T1.C1 = T2.C1
4         #NEST LOOP INNER JOIN2: [9805, 17465, 296]; 
5           #BLKUP2: [1, 175, 148]; IDX_T1_C2(T1)
6             #SSEK2: [1, 175, 148]; scan_type(ASC), IDX_T1_C2(T1), scan_range['A','A']
7           #CSCN2: [1, 10000, 148]; IDX_C1_T2(T2)
 
已用时间: 0.796(毫秒). 执行号:0.

适用场景:

  • 驱动表有很好的过滤条件
  • 表连接条件能使用索引
  • 结果集比较小

3.10 AAGR简单聚集

SQL> EXPLAIN SELECT COUNT(*) FROM T1 WHERE C1 = 10;
 
1   #NSET2: [1, 1, 4] 
2     #PRJT2: [1, 1, 4]; exp_num(1), is_atom(FALSE) 
3       #AAGR2: [1, 1, 4]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #SSEK2: [1, 1, 4]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]
 
已用时间: 0.712(毫秒). 执行号:0.
SQL> 

AAGR用于没有GROUP BY的COUNT、SUM、AGE、MAX、MIN等聚集函数的计算。

3.11FAGR快速聚集

SQL> EXPLAIN  SELECT MAX(C1) FROM T1;
 
1   #NSET2: [1, 1, 4] 
2     #PRJT2: [1, 1, 4]; exp_num(1), is_atom(FALSE) 
3       #FAGR2: [1, 1, 4]; sfun_num(1), IDX_C1_T1
 
已用时间: 0.541(毫秒). 执行号:0.
SQL> 

3.12HASH分组聚集

SQL> EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C2;
 
1   #NSET2: [2, 100, 48] 
2     #PRJT2: [2, 100, 48]; exp_num(1), is_atom(FALSE) 
3       #HAGR2: [2, 100, 48]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(T1.C2) 
4         #CSCN2: [1, 10000, 48]; INDEX33555492(T1)
 
已用时间: 0.480(毫秒). 执行号:0.
SQL>

HAGR 用于分组列没有索引只能走全表扫描的分组聚集,该示例中 C2 列没有创建索引。

3.13 SAGR流分组聚集


SQL> EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C1;
 
1   #NSET2: [2, 100, 4] 
2     #PRJT2: [2, 100, 4]; exp_num(1), is_atom(FALSE) 
3       #SAGR2: [2, 100, 4]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(T1.C1) 
4         #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)
 
已用时间: 0.547(毫秒). 执行号:0.

SAGR 用于分组列是有序的情况下,可以使用流分组聚集,C1 列上已经创建了索引,SAGR2 性能优于 HAGR2。

3.14 HASH JOIN哈希连接

SQL> EXPLAIN select * from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
 
1   #NSET2: [4, 24502, 296] 
2     #PRJT2: [4, 24502, 296]; exp_num(8), is_atom(FALSE) 
3       #HASH2 INNER JOIN: [4, 24502, 296];  KEY_NUM(1); KEY(T1.C1=T2.C1) KEY_NULL_EQU(0)
4         #SLCT2: [1, 250, 148]; T1.C2 = 'A'
5           #CSCN2: [1, 10000, 148]; INDEX33555492(T1)
6         #CSCN2: [1, 10000, 148]; INDEX33555493(T2)
 
已用时间: 0.829(毫秒). 执行号:0.
SQL>

使用较小的Row source 作为Hash table和Bitmap, 而第二个row source被hashed,根据bitmap与第一个row source生成的hash table 相匹配,bitmap查找的速度极快。

hash join特点:

一般没索引或用不上索引时会使用该连接方式。 选择小的表(或row source)做hash表。 只适用等值连接中的情形。

由于hash连接比较消耗内存,如果有很多这种连接时,需调整以下3个参数: HJ_BUF_GLOBAL_SIZE HJ_BUF_SIZE HJ_BLK_SIZE

3.15 MERGE JOIN归并排序连接

EXPLAIN select /*+use_merge(t1 t2)*/t1.c1,t2.c1 from t1 inner join t2 on t1.c1=t2.c1 where t2.c2='b';
1   #NSET2: [13, 24725, 56] 
2     #PRJT2: [13, 24725, 56]; exp_num(2), is_atom(FALSE) 
3       #SLCT2: [13, 24725, 56]; T2.C2 = 'b'
4         #MERGE INNER JOIN3: [13, 24725, 56]; KEY_NUM(1); KEY(COL_0 = COL_0) KEY_NULL_EQU(0)
5           #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)
6           #BLKUP2: [1, 10000, 52]; IDX_T2_C1(T2)
7             #SSCN: [1, 10000, 52]; IDX_T2_C1(T2)

归并排序连接需要两张表的连接列都有索引,对两张表扫描索引后按照索引顺序进行归并。 无驱动表之分,随机读很少。两个表都需要按照连接列排序,需要消耗大量的cpu和额外的内存。 应用场景: 通常情况下,merge sort join需要消耗大量的cpu和内存,效率都不会太高。 如果存在相关索引可以消除sort,那么CBO可能会考虑该连接方式。