更新时间:2024-05-07 GMT+08:00

详解

SQL执行计划概述节中所说,EXPLAIN会显示执行计划,但并不会实际执行SQL语句。EXPLAIN ANALYZE和EXPLAIN PERFORMANCE两者都会实际执行SQL语句并返回执行信息。在这一节将详细解释执行计划及执行信息。

执行计划

以如下SQL语句为例:

1
SELECT * FROM t1, t2 WHERE t1.c1 = t2.c2;
执行EXPLAIN的输出为:
gaussdb=#  EXPLAIN SELECT * FROM t1,t2 WHERE t1.c1 = t2.c2;
                            QUERY PLAN                             
-------------------------------------------------------------------
 Hash Join  (cost=23.73..341.30 rows=16217 width=180)
   Hash Cond: (t1.c1 = t2.c2)
   ->  Seq Scan on t1  (cost=0.00..122.17 rows=5317 width=76)
   ->  Hash  (cost=16.10..16.10 rows=610 width=104)
         ->  Seq Scan on t2  (cost=0.00..16.10 rows=610 width=104)
(5 rows)

执行计划层级解读(纵向):

  1. 第一层:Seq Scan on t2

    表扫描算子,用Seq Scan的方式扫描表t2。这一层的作用是把表t2的数据从buffer或者磁盘上读上来输送给上层节点参与计算。

  2. 第二层:Hash

    Hash算子,作用是把下层计算输送上来的算子计算hash值,为后续hash join操作做数据准备。

  3. 第三层:Seq Scan on t1

    表扫描算子,用Seq Scan的方式扫描表t1。这一层的作用是把表t1的数据从buffer或者磁盘上读上来输送给上层节点参与hash join计算。

  4. 第四层:Hash Join

    join算子,主要作用是将t1表和t2表的数据通过hash join的方式连接,并输出结果数据。

执行计划中的主要关键字说明:

  1. 表访问方式
    • Seq Scan

      全表顺序扫描。

    • Index Scan

      优化器决定使用两步的规划:最底层的规划节点访问一个索引,找出匹配索引条件的行的位置,然后上层规划节点真实地从表中抓取出那些行。独立地抓取数据行比顺序地读取它们的开销高很多,但是因为并非所有表的页面都被访问了,这么做实际上仍然比一次顺序扫描开销要少。使用两层规划的原因是,上层规划节点在读取索引标识出来的行位置之前,会先将它们按照物理位置排序,这样可以最小化独立抓取的开销。

      如果在WHERE里面使用的好几个字段上都有索引,那么优化器可能会使用索引的AND或OR的组合。但是这么做要求访问两个索引,因此与只使用一个索引,而把另外一个条件只当作过滤器相比,这个方法未必是更优。

      索引扫描可以分为以下几类,他们之间的差异在于索引的排序机制。

      • Bitmap Index Scan

        使用位图索引抓取数据页。

      • Index Scan using index_name

        使用简单索引搜索,该方式按照索引键的顺序在索引表中抓取数据。该方式最常用于在大数据量表中只抓取少量数据的情况,或者通过ORDER BY条件匹配索引顺序的查询,以减少排序时间。

      • Index-Only Scan

        当需要的所有信息都包含在索引中时,仅索引扫描便可获取所有数据,不需要引用表。

    • Bitmap Heap Scan

      从其他操作创建的位图中读取页面,过滤掉不符合条件的行。位图堆扫描可避免随机I/O,加快读取速度。

    • TID Scan

      通过TupleID扫描表。

    • Index Ctid Scan

      通过Ctid上的索引对表进行扫描。

    • CTE Scan

      CTE对子查询的操作进行评估并将查询结果临时存储,相当于一个临时表。CTE Scan算子对该临时表进行扫描。

    • Foreign Scan

      从远程数据源读取数据。

    • Function Scan

      获取函数返回的结果集,将它们作为从表中读取的行并返回。

    • Sample Scan

      查询并返回采样数据。

    • Subquery Scan

      读取子查询的结果。

    • Values Scan

      作为VALUES命令的一部分读取常量。

    • WorkTable Scan

      工作表扫描。在操作中间阶段读取,通常是使用WITH RECURSIVE声明的递归操作。

  2. 表连接方式
    • Nested Loop

      嵌套循环,适用于被连接的数据子集较小的查询。在嵌套循环中,外表驱动内表,外表返回的每一行都要在内表中检索找到它匹配的行,因此整个查询返回的结果集不能太大(不能大于10000),要把返回子集较小的表作为外表,而且在内表的连接字段上建议要有索引。

    • (Sonic) Hash Join

      哈希连接,适用于数据量大的表的连接方式。优化器使用两个表中较小的表,利用连接键在内存中建立hash表,然后扫描较大的表并探测散列,找到与散列匹配的行。Sonic和非Sonic的Hash Join的区别在于所使用hash表结构不同,不影响执行的结果集。

    • Merge Join

      归并连接,通常情况下执行性能差于哈希连接。如果源数据已经被排序过,在执行归并连接时,并不需要再排序,此时归并连接的性能优于哈希连接。

  3. 运算符
    • sort

      对结果集进行排序。

    • filter

      EXPLAIN输出显示WHERE子句当作一个"filter"条件附属于顺序扫描计划节点。这意味着规划节点为它扫描的每一行检查该条件,并且只输出符合条件的行。预计的输出行数降低了,因为有WHERE子句。不过,扫描仍将必须访问所有 10000 行,因此开销没有降低,实际上它还增加了一些(确切的说,通过10000 * cpu_operator_cost)以反映检查WHERE条件的额外CPU时间。

    • LIMIT

      LIMIT限定了执行结果的输出记录数。如果增加了LIMIT,那么不是所有的行都会被检索到。

    • Append

      合并子操作的结果。

    • Aggregate

      将查询行产生的结果进行组合。可以是GROUPBY、UNION、SELECT DISTINCT子句等函数的组合。

    • BitmapAnd

      位图的AND操作,通过该操作组成匹配更复杂条件的位图。

    • BitmapOr

      位图的OR操作,通过该操作组成匹配更复杂条件的位图。

    • Gather

      将并行线程的数据汇总。

    • Group

      对行进行分组,以进行GROUP BY操作。

    • GroupAggregate

      聚合GROUP BY操作的预排序行。

    • Hash

      对查询行进行散列操作,以供父查询使用。通常用于执行JOIN操作。

    • HashAggregate

      使用哈希表聚合GROUP BY的结果行。

    • Merge Append

      以保留排序顺序的方式对子查询结果进行组合,可用于组合表分区中已排序的行。

    • ProjectSet

      对返回的结果集执行函数。

    • Recursive Union

      对递归函数的所有步骤进行并集操作。

    • SetOp

      集合运算,如INTERSECT或EXCEPT。

    • Unique

      从有序的结果集中删除重复项。

    • HashSetOp

      一种用于 INTERSECT 或 EXCEPT 等集合操作的策略,它使用 Append 来避免预排序的输入。

    • LockRows

      锁定有问题的行以阻止其他查询写入,但允许读。

    • Materialize

      将子查询的结果存储在内存里,以方便父查询快速访问获取。

    • Result

      在不进行扫描的情况下返回一个值(比如硬编码的值)。

    • WindowAgg

      窗口聚合函数,一般由OVER语句触发。

    • Merge

      归并操作。

    • StartWith Operator

      层次查询算子,用于执行递归查询操作。

    • Rownum

      对查询结果的行编号进行条件过滤。通常出现在rownum子句里。

    • Index Cond

      索引扫描条件。

    • Unpivot

      转置算子。

  4. 分区剪枝相关信息
    • Iterations

      分区迭代算子对一级分区的迭代次数。如果显示PART则为动态剪枝场景。

      例如:Iterations: 4表示迭代算子需要遍历4个一级分区。Iterations: PART表示遍历一级分区个数需要由分区键上的参数条件决定。

    • Selected Partitions

      一级分区剪枝的结果,m..n表示m到n号分区被剪枝选中,多个不连续的分区由逗号连接。

      例如:Selected Partitions: 2..4,7 表示2、3、4、7四个分区被选中。

    • Sub Iterations

      分区迭代算子对二级分区的迭代次数。如果显示PART则为动态剪枝场景。

      例如:Sub Iterations: 4表示迭代算子需要遍历4个二级分区。Iterations: PART表示遍历二级分区个数需要由分区键上的参数条件决定。

    • Selected Subpartitions

      二级分区被剪枝的结果,由一级分区序号:二级分区序号。

      例如:Selected Subpartitions: 2:1 3:2 表示第二个一级分区的1号二级分区和第三个一级分区的2号二级分区被选中。Selected Subpartitions: ALL表示所有二级分区均被选中。

  5. 其他关键字
    • Partitioned

      对具体分区的操作。

    • Partition Iterator

      分区迭代器,通常代表子查询是对分区的操作。

    • InitPlan

      非相关子计划。

执行信息

以如下SQL语句在pretty模式下的执行结果为例:

gaussdb=# select sum(t2.c1) from t1,t2 where t1.c1=t2.c2 group by t1.c2;

执行EXPLAIN PERFORMANCE输出为:

gaussdb=# explain performance select sum(t2.c1) from t1,t2 where t1.c1=t2.c2 group by t1.c2;
 id |             operation              | A-time | A-rows | E-rows | E-distinct | Peak Memory | A-width | E-width |     E-costs      
----+------------------------------------+--------+--------+--------+------------+-------------+---------+---------+------------------
  1 | ->  HashAggregate                  | 0.574  |      0 |    200 |            | 29KB        |         |       8 | 396.113..398.113
  2 |    ->  Hash Join (3,4)             | 0.358  |      0 |  18915 | 200, 200   | 12KB        |         |       8 | 53.763..301.538
  3 |       ->  Seq Scan on public.t1    | 0.037  |      1 |   1945 |            | 22KB        |         |       8 | 0.000..29.450
  4 |       ->  Hash                     | 0.038  |      0 |   1945 |            | 264KB       |         |       8 | 29.450..29.450
  5 |          ->  Seq Scan on public.t2 | 0.029  |     30 |   1945 |            | 22KB        |         |       8 | 0.000..29.450
(5 rows)

 Predicate Information (identified by plan id) 
-----------------------------------------------
   2 --Hash Join (3,4)
         Hash Cond: (t1.c1 = t2.c2)
(2 rows)

    Memory Information (identified by plan id)    
--------------------------------------------------
   1 --HashAggregate
         Peak Memory: 29KB, Estimate Memory: 64MB
   2 --Hash Join (3,4)
         Peak Memory: 12KB, Estimate Memory: 64MB
   3 --Seq Scan on public.t1
         Peak Memory: 22KB, Estimate Memory: 64MB
   4 --Hash
         Peak Memory: 264KB
  Buckets: 32768  Batches: 1  Memory Usage: 0kB
   5 --Seq Scan on public.t2
         Peak Memory: 22KB, Estimate Memory: 64MB
(11 rows)

 Targetlist Information (identified by plan id) 
------------------------------------------------
   1 --HashAggregate
         Output: sum(t2.c1), t1.c2
         Group By Key: t1.c2
   2 --Hash Join (3,4)
         Output: t1.c2, t2.c1
   3 --Seq Scan on public.t1
         Output: t1.c1, t1.c2, t1.c3
   4 --Hash
         Output: t2.c1, t2.c2
   5 --Seq Scan on public.t2
         Output: t2.c1, t2.c2
(11 rows)

                               Datanode Information (identified by plan id)                               
----------------------------------------------------------------------------------------------------------
   1 --HashAggregate
         (actual time=0.574..0.574 rows=0 loops=1)
         (Buffers: shared hit=2)
         (CPU: ex c/r=0, ex row=0, ex cyc=527797, inc cyc=8385141377087373)
   2 --Hash Join (3,4)
         (actual time=0.358..0.358 rows=0 loops=1)
         (Buffers: shared hit=2)
         (CPU: ex c/r=-8385141375712241, ex row=1, ex cyc=-8385141375712241, inc cyc=8385141376559576)
   3 --Seq Scan on public.t1
         (actual time=0.037..0.037 rows=1 loops=1)
         (Buffers: shared hit=1)
         (CPU: ex c/r=8385141375728512, ex row=1, ex cyc=8385141375728512, inc cyc=8385141375728512)
   4 --Hash
         (actual time=0.038..0.038 rows=0 loops=1)
         (Buffers: shared hit=1)
         (CPU: ex c/r=0, ex row=0, ex cyc=-251554241295571040, inc cyc=8385141376543305)
   5 --Seq Scan on public.t2
         (actual time=0.019..0.029 rows=30 loops=1)
         (Buffers: shared hit=1)
         (CPU: ex c/r=8664646089070478, ex row=30, ex cyc=259939382672114336, inc cyc=259939382672114336)
(20 rows)

       ====== Query Summary =====       
----------------------------------------
 Datanode executor start time: 0.180 ms
 Datanode executor run time: 0.590 ms
 Datanode executor end time: 0.051 ms
 Planner runtime: 0.366 ms
 Query Id: 844424930141239
 Total runtime: 0.866 ms
(6 rows)

上述示例中显示执行信息分为以下6个部分:

  1. 以表格的形式将计划显示出来,包含有11个字段,分别是:id、operation、A-time、A-rows、E-rows、E-distinct、Peak Memory、E-memory、A-width、E-width和E-costs。其中计划类字段(id、operation以及E开头字段)的含义与执行EXPLAIN时的含义一致,详见执行计划小节中的说明。A-time、A-rows、E-distinct、Peak Memory、A-width的含义说明如下:
    • A-time:当前算子执行完成时间。
    • A-rows:表示当前算子的实际输出元组数。
    • E-distinct:表示hashjoin算子的distinct估计值。
    • Peak Memory:此算子在执行时使用的内存峰值。
    • A-width:表示当前算子每行元组的实际宽度,仅对于重内存使用算子会显示,包括:(Vec)HashJoin、(Vec)HashAgg、(Vec) HashSetOp、(Vec)Sort、(Vec)Materialize算子等,其中(Vec)HashJoin计算的宽度是其右子树算子的宽度,会显示在其右子树上。
  2. Predicate Information (identified by plan id):

    这一部分主要显示的是静态信息,即在整个计划执行过程中不会变的信息,主要是一些join条件和一些filter信息。

  3. Memory Information (identified by plan id):

    这一部分显示的是整个计划中会将内存的使用情况打印出来的算子的内存使用信息,主要是Hash、Sort算子,包括算子峰值内存(peak memory),控制内存(control memory),估算内存使用(operator memory),执行时实际宽度(width),内存使用自动扩展次数(auto spread num),是否提前下盘(early spilled),以及下盘信息,包括重复下盘次数(spill Time(s)),内外表下盘分区数(inner/outer partition spill num),下盘文件数(temp file num),下盘数据量及最小和最大分区的下盘数据量(written disk IO [min, max] )。

  4. Targetlist Information (identified by plan id):

    这一部分显示的是每一个算子输出的目标列。

  5. DataNode Information (identified by plan id):

    这一部分会将各个算子的执行时间、CPU、buffer的使用情况全部打印出来。

  6. ====== Query Summary =====:

    这一部分主要打印总的执行时间和网络流量,包括了初始化和结束阶段的最大最小执行时间,以及当前语句执行时系统可用内存、语句估算内存等信息。