数据库性能调优技术系列文章(2)--深入理解单表执行计划

时间:2022-09-19 03:36:26
  
数据库性能调优技术系列文章(2)
                             -- 深入理解单表执行计划
 
                              作者:杨万富
 
一、概述
      这篇文章是数据库性能调优技术的第二篇。上一篇讲解的索引调优是数据库性能调优技术的基础。这篇讲解的深入理解单表执行计划,是数据库性能调优的有力工具。
      查询语句可以有多种可选执行计划,如何选择效率最高的执行计划 ? 达梦数据库、 oracle 数据库、 sql server 数据库都是采用基于成本的查询优化,对备选执行计划进行打分,选择代价最小的执行计划进行执行。这些内容,我会在后续的几篇文章中进行详细的描述。在此之前,我们首先需要掌握如何理解数据库执行计划。这篇文章讲解只涉及单表操作的执行计划。
达梦数据库、 oracle 数据库、 sql server 数据库都可以显示给定语句的执行计划。我详细分析了这三个数据库的执行计划,三者之间并无本质区别。
所以本文的内容适合于这三个数据库。同样,也应该适合绝大多数其它的数据库。
单表执行的深入理解,是了解多表执行计划的基础。达梦数据库显示的执行计划时,显示的信息会多一些。
因此,这篇文章中我选择达梦数据库作为实例数据库来讲解执行计划的原理。
读完本文后,应该能够读懂这三个数据库的单表执行计划。
 
 
二、深入理解数据库执行计划
达梦数据库的执行计划有两种显示方式:第一种为图形化的显示方式;第二种为文本式的显示方式。这里采用第二种方式进行讲解。
理解执行计划,是迈向理解数据库性能调优的重要一步。从执行计划中,我们可以看出数据库是如何执行查询语句,并根据执行计划判断出该查询语句的执行是否高效,以及如何进行优化。
      下面我们将通过一些例子来理解数据库执行计划。
 
1.没有索引的全表扫描过滤如何执行?
   构造处执行场景:
create table t1(c1 int,c2 int);
insert into t1 values(1,1);
insert into t1 values(2,2);
insert into t1 values(3,3);
insert into t1 values(4,4);
insert into t1 values(5,5);
insert into t1 values(6,6);
   查询语句为:
select * from t1 where c1=2;
   该语句的执行过程,如果用语言描述可以描述成这样:
1 )如果是第一次执行该步骤,则取得表的第一条记录;否则取得当前记录的下一条记录。如果记录已经扫描结束,则执行步骤 4 ,否则执行步骤 2
2 )判断该记录是否满足过滤条件 c1=2 ,满足则执行步骤 3 ,否则执行步骤 1
3 )把该记录放到结果集中,执行步骤 1
4 )将结果集返回给客户端。
   实际上,数据库执行查询语句的过程也是类似的,下面是该查询语句的执行计划:
#RSET:[21, 1, 1];
        #XFLT:[0, 0, 0]; EXPR0 = 2
                #CSEK:[21, 1, 1]; INDEX33555545(T1), FULL_SCAN
   该执行计划中出现的内容,在此做出解释:
1)CSEK (查找)类似于上文中描述的步骤 1 ,方括号中的内容是执行该操作的评估代价,本文不作分析。“ INDEX33555545(T1) ”说明使用了 T1 表的聚集索引,“ FULL_SCAN ”表示对聚集索引 INDEX33555545(T1) 进行全扫描。
   这里需要注意的是,达梦数据库中的表默认情况下是索引组织的。如果建表时指定了 cluster primary key ,那么数据以该 clsuter primary key 组织数据,否则以 rowid 组织数据。
2 XFLT (过滤)类似于上文中描述的步骤 2 ,“ EXPR0 = 2 ”是过滤条件。
3 RSET (结果集)类似于上文中描述的步骤 3 ,用来存放符合条件的记录集。
 
   我们可以看出,数据库的执行过程和我们用语言描述的步骤是一致的。
   该查询语句完整的执行流程如下:
1 CSEK 取得第一条记录( 1 1 )传给 XFLT ,将控制权传给 XFLT
2 XFLT 发现该记录( 1 1 )不符合条件,将控制权传给 CSEK
3 CSEK 取得下一条记录( 2 2 )传给 XFLT ,将控制权传给 XFLT
4 XFLT 发现记录( 2 2 )符合条件,将该记录传给 RSET ,将控制权传给 RSET
5 RSET 将记录( 2 2 )放入结果集,将控制权传给 XFLT
6 XFLT 给控制权传给 CSEK
7 CSEK 取得下一条( 3 3 )传给 XFLT ,将控制权传给 XFLT
8 XFLT 发现该记录( 3 3 )不符合条件,将控制权传给 CSEK
9 CSEK 取得下一条( 4 4 )传给 XFLT ,将控制权传给 XFLT
10 XFLT 发现该记录( 4 4 )不符合条件,将控制权传给 CSEK
11 CSEK 取得下一条( 5 5 )传给 XFLT ,将控制权传给 XFLT
12 XFLT 发现该记录( 5 5 )不符合条件,将控制权传给 CSEK
13 CSEK 取得下一条( 6 6 )传给 XFLT ,将控制权传给 XFLT
14 XFLT 发现该记录( 6 6 )不符合条件,将控制权传给 CSEK
15 CSEK 发现描述操作已经结束,通知 XFLT 结束。将控制权传给 XFLT
16 XFLT 得知查询操作结束,通知 RSET 结束。将控制权传给 RSET
17 RSET 得知操作结束。
18 )发送结果集(包含记录( 2 2 ))给客户端。
 
 
2.如果表t1上的c1列有非唯一索引,如何执行呢?
   t1 的定义以及数据和 1 中描述的一样。
   创建索引:
create index it1c1 on t1(c1);
查询语句“ select * from t1 where c1=2; ”对应的执行计划为:
#RSET:[201, 2, 1];
        #CSEK(SECOND):[201, 2, 1]; IT1C1(T1), INDEX_EQU_SEARCH
       CSEK 行的“ SECOND ”表示使用非聚集索引“ IT1C1 ”,对该索引进行索引等值( INDEX_EQU_SEARCH )查找。
      该执行计划的执行流程为:
1 CSEK 使用 c1=2 查找非聚集索引,得到第一条 c1=2 的索引记录( 2 rowid1 )中的 rowid1 (为数值)。使用 rowid1 查找聚集索引得到对应的数据记录( 2 2 )传递给 RSET ,将控制权传给 RSET
2 RSET 将记录( 2 2 )放入结果集,将控制权传给 CSEK 。(因为 c1 上的索引是非唯一的,所以可能出现两条以上的记录满足 c1=2 ,所以需要将控制权传给 CSEK )。
3 CSEK 取得当前非聚集记录的下一条记录( 3 rowid2 ),因为 3!=2 ,所以扫描结束。将控制权传给 RSET 。(如果满足 c1=2 的记录数大于 1 条,需要继续传递记录给 RSET ,以此类推,直到遇到不满足 c1=2 的那条记录,结束操作。)
4 RSET 得知操作结束。
5 )发送结果集(包含记录( 2 2 ))给客户端。
 
 
3.如果表t1上的c1列有唯一索引,如何执行呢?
   首先删除 c1 列上的非唯一索引,然后在 c1 列上创建唯一索引:
drop index it1c1;
create unique index uit1c1 on t1(c1);
   查询语句“ select * from t1 where c1=2; ”对应的执行计划为:
#RSET:[201, 2, 1];
        #CSEK(SECOND):[201, 2, 1]; UIT1C1(T1), INDEX_EQU_SEARCH
   该执行计划的执行流程为:
1 CSEK 使用 c1=2 查找非聚集索引,得到 c1=2 的索引记录( 2 rowid1 )中的 rowid1 (为数值)。使用 rowid1 查找聚集索引得到对应的数据记录( 2 2 )传递给 RSET ,将控制权传给 RSET 。(当然,有人也许会问,如果没有记录满足 c1=2 怎么办呢?那么,此处什么记录都不传递给 RSET ,通知 RSET 查询操作结束,最后返回空集给客户端)。
2 RSET 将记录( 2 2 )放入结果集,操作结束(因为是唯一索引,所以最多只有 1 条记录满足 c1=2 )。
3 )发送结果集(包含记录( 2 2 ))给客户端。
     这里我们发现,例 3 使用了唯一索引,例 2 使用了非唯一索引。例 3 的执行速度大于例 2 的执行速度。
 
4.如何理解执行计划中的top n操作?
   查询语句“ select top 10 * from t1 where c1>2; ”对应的执行计划为:
#RSET:[21, 1, 1];
        #XTOP:[0, 0, 0]; top_off(0), top_num(10)
                #XFLT:[0, 0, 0]; EXPR1 > 2
                        #CSEK:[21, 1, 1]; INDEX33555545(T1), FULL_SCAN
     XTOP (取得前 N 条记录):将 XFLT 操作符传递来的记录放入到 RSET (结果集)中,并判断记录数是否已经等于给定值 10 (语句中的 top 10 )。如果已经等于 10 ,则查询已经执行成功,退出。否则将控制权限传给 XFLT ,继续执行。依次执行,直到取得 10 条记录,或者表 CSEK 操作已经查询结束(即符合条件的记录不满 10 条)。
 
5.如何理解执行计划中的order by操作?
   查询语句“ select top 10 * from t1 where c2>2 order by c1; ”对应的执行计划为:
#RSET:[21, 1, 1];
        #XSORT:[0, 0, 0]; keys_num(1), is_distinct(FALSE)
                #XFLT:[0, 0, 0]; EXPR1 > 2
                        #CSEK:[21, 1, 1]; INDEX33555545(T1), FULL_SCAN
    XSORT (对记录进行排序):将 XFLT 操作符传递来的记录插入到 XSORT 维护的临时空间中的合理位置,按 c1 进行有序排列。然后将控制权传给 XFLT 以取得下一条符合条件的记录。等处理完所有符合条件的记录。 XSORT 操作符才会将控制权限传给 RSET
 
6.是不是查询语句中一旦出现order by字句,执行计划中就会出现XSORT操作符?
  不是。
  比如,查询语句“ select c1 from t1 order by c1; ”对应的执行计划为:
#RSET:[0, 0, 0];
        #CSEK:[0, 0, 0]; UIT1C1(T1), FULL_SCAN
  从执行中我们可以看出,达梦直接对索引 UIT1C1 进行全索引扫描,对于得到的每一条记录不需要进行 XSORT 排序操作,直接放入 RSET (结果集)中。因为索引 UIT1C1 本身就是按照 c1 进行排序的。
 
7.有文档说,对于语句“select max(c1) from t1”,可以在c1列上创建索引从而查询速度变快。那么在执行计划中是如何体现的呢?
     查询语句“ select max(c1) from t1 ”对应的执行计划:
#RSET:[0, 0, 0];
        #XEVL:[0, 0, 0];
                #FAGR:[0, 0, 0]; function_num(1)
     在这个执行计划中,我们没有看到 CSEK 操作符。因为 c1 上存在索引 UIT1C1 ,该索引叶子节点的最右端就是 c1 的最大值。 FARG 直接返回该最大值。语句“ select min(c1) from t1; ”、语句“ select count(*) from t1; ”的执行原理一样。 XEVL 是表达式计算,本文不进行讲解。
 
8.如果列上存在索引,如何理解中的group by操作?
    查询语句“ select c1,count(*) from t1 where c1>=2 group by c1; ”对应的执行计划为:
#RSET:[11, 1, 1];
        #XEVL:[0, 0, 0];
                #SAGR:[0, 0, 0]; group_by_num(1), function_num(1)
                        #CSEK:[11, 1, 1]; UIT1C1(T1), INDEX_GE_SEARCH
     我们可以得到, CSEK 使用了索引 UIT1C1 进行了范围查找。首先传递给 SARG 的是连续的 c1=2 的记录组,然后是 c1=3 的记录组,然后是 c1=4 的记录组,……
     此处 SARG 的执行流程是
1 )从 CSEK 取得一条 c1=2 记录,将计数加 1
2 )从 CSEK 取得下一条记录,如果该记录满足 c1=2 ,将计数 +1
3 )重复执行步骤 2 ,直到取得第一条不满足 c1=2 的记录,将 (2, 对应的计算 ) 传递给 XEVL ,再传给 RSET (结果集)。接着对 c1=3 的记录组执行同样的流程。依此类推,直到处理完所有符合条件的记录。
     这里我们的分组函数是 count(*) ,如果是其它的分组函数,处理过程类似。
 
9.如果列上不存在索引,如何理解中的group by操作?
   查询语句“ select c2,count(*) from t1 where c2>=2 group by c2; ”对应的执行计划为:
#RSET:[21, 1, 1];
     #XEVL:[0, 0, 0];
           #HAGR:[0, 0, 0]; group_by_num(1), function_num(1)
                 #XFLT:[0, 0, 0]; EXPR0 >= 2
                        #CSEK:[21, 1, 1]; INDEX33555550(T1), FULL_SCAN
     这里因为 c2 上没有索引, HARG 的作用是 HASH 分组。
     HARG 的执行流程是:
1 )从 XFLT 取得一条记录
2 )记录的 c1=m ,如果在 hash 表中已经对应项,计数 +1 ,如果不存在对应项,在创建一个新的 hash 项。
3 )所有的符合过滤条件的记录处理完成之后, HARG 才会将控制权限传给上层操作符, HARG 每次向上层操作符传递一条( m,m 对应的计数)。
  这里我们的分组函数是 count(*) ,如果是其它的分组函数,处理过程类似。