概述
在数据库查询中,难免有SQL语句执行时间不满足预期,这个时候需要查看SQL执行计划,以便对SQL进行优化。下面将介绍达梦数据库如何查看执行计划及涉及相关工具。
1.执行计划查看顺序和说明
1.1 执行计划查看顺序
执行计划就是一条 SQL 语句在数据库中的执行过程或访问路径的描述。在遇到数据库性能问题时,往往需要查看慢SQL执行计划,下面就是一个例子:
缩进越多的越先执行,同样缩进的上面的先执行,
下面的后执行,上下的优先级高于内外。
缩进最深的,最先执行;
缩进深度相同的,先上后下。
口诀:最右最上先执行。
1.2 执行计划查看说明
以上面1.1执行计划输出内容为例,执行计划的每行即为一个计划节点,主要包含三部分信息。
- 第1部分 NEST2、PRJT2、CSCN2 为操作符及数据库具体执行了什么操作。
- 第2部分 三元组为该计划节点的执行代价,具体含义为[代价,记录行数,字节数]。
- 第3部分 为操作符的补充信息。
less
#CSCN2: [1, 856, 285]; INDEX33555892(EMPLOYEE)
操作符,[代价,行数,字节数] 描述
以上面一行输出信息为例进行说明,操作符进行CSCN2了全表扫描,估算代价是1,行数是856行,涉及285个字节。
2.通过什么工具查看执行计划
2.1 通过 DM 数据库管理工具查看
如下图所示,选择查询语句后,点击按钮【显示执行计划P】或者执行【F9】即可查看SQL语句执行计划。
2.2 通过 explain 命令查看
通过disql 登录到数据库中,使用explain命令查看执行计划,如下图所示:
3. 操作符说明
达梦中执行计划涉及到的一些主要操作符有如下表格,因执行计划操作都是英文缩写,为更好的理解操作符操作缩写含义,专门对其缩写前的英文描述进行整理。
接下来结合实例来介绍下这些操作符:
准备测试数据:
sql
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收集结果集
less
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投影
less
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选择
ini
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全表扫描
less
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二次扫描
scss
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二级索引扫
scss
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索引全扫描
scss
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聚集索引扫描
scss
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嵌套循环连接
less
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 的连接列创建索引,并收集统计信息,语句如下所示:
scss
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');
再次查看执行计划,执行成本明显下降:
less
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简单聚集
scss
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快速聚集
scss
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分组聚集
scss
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流分组聚集
scss
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哈希连接
less
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归并排序连接
less
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可能会考虑该连接方式。