DM 执行计划解读

本章节将对 DM 数据库中的执行计划做简要介绍,希望读者在看完本章节后能够对执行计划有基本认识,可以通过执行计划对 SQL 语句执行效率做出大致判断。

何为执行计划

简单来说,执行计划就是一条 SQL 语句在数据库中的执行过程或访问路径的描述。SQL 语言是种功能强大且非过程性的编程语言,比如以下这条 SQL 语句:

SELECT * FROM T1, T2 WHERE T1.ID = T2.ID AND T1.ID = 6;

开发人员只关心 SQL 语句能否返回 T1 与 T2 表的关联查询结果,不需要指定该 SQL 如何执行,也就是说不关心该 SQL 是先访问 T1 表还是先访问 T2 表。对于 SQL 来说,两种访问方式就是两个执行计划,查询优化器 (CBO) 将根据代价也就是开销来选择最优的执行计划,执行计划如下:

SELECT * FROM SYSOBJECTS;

1   #NSET2: [0, 1282, 396]
2     #PRJT2: [0, 1282, 396]; exp_num(17), is_atom(FALSE)
3       #CSCN2: [0, 1282, 396]; SYSINDEXSYSOBJECTS(SYSOBJECTS as SYSOBJECTS)
  • 执行计划的每行即为一个计划节点,主要包含三部分信息。
  • 第一部分 NEST2、PRJT2、CSCN2 为操作符及数据库具体执行了什么操作。
  • 第二部分的三元组为该计划节点的执行代价,具体含义为[代价,记录行数,字节数]。
  • 第三部分为操作符的补充信息。

例如:第三个计划节点:操作符是 CSCN2 即全表扫描,代价估算是 0 ms,扫描的记录行数是 1282 行,输出字节数是 396 个。

各计划节点的执行顺序为:缩进越多的越先执行,同样缩进的上面的先执行,下面的后执行,上下的优先级高于内外。

如何查看执行计划

查看执行计划主要两种方式,通过 DM 数据库配套管理工具查看或使用 SQL 语句查看。下面具体介绍两种查看方式。

管理工具查看执行计划

在 DM 配套管理工具中,选中待查看执行计划的 SQL 语句,点击下图中按钮,或使用快捷键 F9,即可查看执行计划,如下图所示:

管理工具查看执行计划

SQL 方式查看执行计划

使用 SQL 方式同样可查看执行计划,在待查看执行计划的 SQL 语句前加 explain 执行 SQL 语句即可查看执行计划,如下图所示:

 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');

NSET:结果集收集

 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]; INDEX33556710(T1

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

PRJT:投影

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]; INDEX33556710(T1

关系的【投影】 (project) 运算,用于选择表达式项的计算。广泛用于查询,排序,函数索引创建等。优化工作中无需对该操作符过多关注,一般没有优化空间。

SLCT:选择

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]; INDEX33556717(T1)

关系的【选择】运算,用于查询条件的过滤。可比较返回结果集与代价估算中是否接近,如相差较大可考虑收集统计信息。若该过滤条件过滤性较好,可考虑在条件列增加索引。

AAGR:简单聚集

EXPLAIN SELECT COUNT(*) FROM T1 WHERE C1 = 10;

1   #NSET2: [0, 1, 4]
2     #PRJT2: [0, 1, 4]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [0, 1, 4]; grp_num(0), sfun_num(1)
4         #SSEK2: [0, 1, 4]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

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

SAGR:快速聚集

EXPLAIN  SELECT MAX(C1) FROM T1;

1   #NSET2: [1, 1, 0]
2     #PRJT2: [1, 1, 0]; exp_num(1), is_atom(FALSE)
3       #FAGR2: [1, 1, 0]; sfun_num(1)

用于没有过滤条件时从表或索引快速获取 MAX、MIN、COUNT 值。

HAGR:HASH 分组聚集

EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C2;

1   #NSET2: [1, 100, 48]
2     #PRJT2: [1, 100, 48]; exp_num(1), is_atom(FALSE)
3       #HAGR2: [1, 100, 48]; grp_num(1), sfun_num(1)
4         #CSCN2: [1, 10000, 48]; INDEX33556717(T1)

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

SAGR:流分组聚集

EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C1;

1   #NSET2: [1, 100, 4]
2     #PRJT2: [1, 100, 4]; exp_num(1), is_atom(FALSE)
3       #SAGR2: [1, 100, 4]; grp_num(1), sfun_num(1)
4         #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)

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

BLKUP:二次扫描 (回表)

EXPLAIN SELECT * FROM T1 WHERE C1=10;

1   #NSET2: [0, 1, 156]
2     #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3       #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

先使用二级索引索引定位 rowid,再根据表的主键、聚集索引、rowid 等信息获取数据行中其它列。

CSCN:全表扫描

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]; INDEX33556710(T1)

CSCN2 是 CLUSTER INDEX SCAN 的缩写即通过聚集索引扫描全表,全表扫描是最简单的查询,如果没有选择谓词,或者没有索引可以利用,则系统一般只能做全表扫描。全表扫描 I/O 开销较大,在一个高并发的系统中应尽量避免全表扫描。

SSEK、CSEK、SSCN:索引扫描

-- 创建所需索引
CREATE CLUSTER INDEX IDX_C1_T2  ON T2(C1);
CREATE  INDEX IDX_C1_C2_T1  ON T1(C1,C2);
EXPLAIN SELECT * FROM T1 WHERE C1=10;

1   #NSET2: [0, 1, 156]
2     #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3       #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

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

EXPLAIN SELECT * FROM T2 WHERE C1=10;

1   #NSET2: [0, 250, 156]
2     #PRJT2: [0, 250, 156]; exp_num(5), is_atom(FALSE)
3       #CSEK2: [0, 250, 156]; scan_type(ASC), IDX_C1_T2(T2), scan_range[10,10]

CSEK2 是聚集索引扫描只需要扫描索引,不需要扫描表,即无需 BLKUP 操作,如果 BLKUP 开销较大时,可考虑创建聚集索引。

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)

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

NEST LOOP:嵌套循环连接

嵌套循环连接最基础的连接方式,将一张表(驱动表)的每一个值与另一张表(被驱动表)的所有值拼接,形成一个大结果集,再从大结果集中过滤出满足条件的行。驱动表的行数就是循环的次数,将在很大程度上影响执行效率。

连接列是否有索引,都可以走 NEST LOOP,但没有索引,执行效率会很差,语句如下所示:

select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [281359, 24502, 104]
2     #PRJT2: [281359, 24502, 104]; exp_num(4), is_atom(FALSE)
3       #NEST LOOP INNER JOIN2: [281359, 24502, 104]
4         #SLCT2: [5, 250, 52]; T1.C2 = A
5           #CSCN: [5, 10000, 52]; INDEX33555947(T1)
5         #CSCN: [5, 10000, 52]; INDEX33555948(T2)

可针对 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');

再次查看执行计划可看出效率明显改善,代价有显著下降,语句如下所示:

select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [1, 17151, 104]
2     #PRJT2: [1, 17151, 104]; exp_num(4), is_atom(FALSE)
3       #NEST LOOP INDEX JOIN2: [1, 17151, 104]
4         #BLKUP2: [0, 175, 52]; IDX_T1_C2(T1)
5           #SSEK: [0, 175, 52]; scan_type(ASC), IDX_T1_C2(T1), scan_range[A,A]
6         #BLKUP2: [0, 175, 52]; IDX_T2_C1(T2)
7           #SSEK: [0, 175, 52]; scan_type(ASC), IDX_T2_C1(T2), scan_range[T1.C1,T1.C1]

适用场景:

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

HASH JOIN:哈希连接

哈希连接是在没有索引或索引无法使用情况下大多数连接的处理方式。哈希连接使用关联列去重后结果集较小的表做成 HASH 表,另一张表的连接列在 HASH 后向 HASH 表进行匹配,这种情况下匹配速度极快,主要开销在于对连接表的全表扫描以及 HASH 运算。

select * from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [12, 24502, 104]
2     #PRJT2: [12, 24502, 104]; exp_num(4), is_atom(FALSE)
3       #HASH2 INNER JOIN: [12, 24502, 104]; KEY_NUM(1);
4         #SLCT2: [5, 250, 52]; T1.C2 = A
5           #CSCN: [5, 10000, 52]; INDEX33555947(T1)
5         #CSCN: [5, 10000, 52]; INDEX33555948(T2)

哈希连接比较消耗内存如果系统有很多这种连接时,需调整以下 3 个参数:

参数名 说明
HJ_BUF_GLOBAL_SIZE HASH 连接操作符的数据总缓存大小 ()>=HJ_BUF_SIZE),系统级参数,以兆为单位。有效值范围(10~500000)
HJ_BUF_SIZE 单个哈希连接操作符的数据总缓存大小,以兆为单位。有效值范围(2~100000)
HJ_BLK_SIZE 哈希连接操作符每次分配缓存 (BLK) 大小,以兆为单位,必须小于 HJ_BUF_SIZE。有效值范围(1~50)

MERGE JOIN:归并排序连接

归并排序连接需要两张表的连接列都有索引,对两张表扫描索引后按照索引顺序进行归并。

-- 对连接列创建索引
CREATE INDEX IDX_T1_C1C2 ON T1(C1,C2);
sselect /*+use_merge(t1 t2)*/t1.c1,t2.c1 from t1 inner join t2 on t1.c1=t2.c1 where t2.c2='b';
1   #NSET2: [12, 24502, 104]
2     #PRJT2: [12, 24502, 104]; exp_num(4), is_atom(FALSE)
3       #MERGE INNER JOIN: [12, 24502, 104]; KEY_NUM(1);
4         #SSCN: [4, 10000, 4]; IDX_T1_C1C2(T1)
5         #BLKUP2: [5, 10000, 52]; IDX_T2_C1(T2)
5           #SSCN: [5, 10000, 52]; IDX_T2_C1(T2)
微信扫码
分享文档
扫一扫
联系客服