执行计划简介

优化器为每个查询设计一个执行计划,针对不同的查询结构和数据属性选择合适的执行计划对提高查询性能非常关键。

输出执行计划

可以通过在语句前添加 explain [analyze]关键字来查看执行计划

../../../../../_images/image.png

算子名称和算子属性可以在 算子介绍 章节查看各算子的名称、介绍、属性等详细信息。

优化器选出总代价最低计划的相关信息

  • cost 算子代价,第一个数值为算子的启动代价,第二个数值为算子的总代价。
  • rows 估算算子会返回的行数,来自统计信息。
  • width 算子返回行的平均宽度,来自统计信息。

代价 是优化器内部为了衡量计划的优劣来定制的一个数值,代价理论上应该跟执行时间成正比,即代价最小计划的执行时间也应该是最快的,用户不需要关注此变量的计算方式

统计信息 是通过执行analyze语句,收集表以及表中每一列的信息,包括平均列宽度,NULL值,表行数,不重复值个数,列高频值,列等频值,数据相关性等

注解

  • rows=8192说明这个表有可能没有收集统计信息,具体可以搜索analyze语句,没有收集统计信息的表默认估算行数就是8192,这可能会导致计划并非最优计划

添加analyze关键字会输出算子实际执行的相关信息

  • actual time 算子执行一次的时间,第一个数值为算子拿到第一行数据的时间,第二个数值为算子执行一次循环(Loop)的时间,单位为ms。
  • rows 算子真实返回的行数,不同于前面的rows,执行器统计了实际返回的行数。
  • loops 算子执行的次数。
  • Planning Time 生成计划时间,单位为ms
  • Execution Time 执行时间,单位为ms

注解

  • cost和actual time没有计算关系,只能说理论上代价大的执行时间会长,代价小的执行时间会短。
  • loops >1时,actual time则等于 执行此算子的时间总和 / loops。
  • 算子执行信息如果是(never executed),代表算子未被执行。
  • 理想状态下,算子的估算行数应该跟真实行数相差不大,这代表优化器正常工作了。
  • 包含ANALYZE选项,EXPLAIN将实际执行执行计划。所以,当执行的是Insert/update/delete等命令时,将实际改变数据,应该谨慎使用。
  • 算子的代价等于子算子的代价加上算子本身运行的代价,同样的算子的执行时间等于所有子算子的时间加上算子本身的执行时间。

举例说明

-- t1数据1,2,3,4,5,6,7,8,9,10
explain select * from t1;
              QUERY PLAN
------------------------------------------------------
 Seq Scan on T1  (cost=0.00..7.10 rows=10 width=4)
(1 row)

SQL为查询T1表的数据,查询计划通过SeqScan(顺序扫描算子)来获取T1表数据

-- 现在为查询加上WHERE子句a = 1
explain select * from t1 where a = 1;
                       QUERY PLAN
---------------------------------------------------------
 Seq Scan on T1  (cost=0.00..7.13 rows=1 width=4)
   Scan Key: (A = 1)
(2 rows)

为SQL添加WHERE条件,查询计划通过在扫描算子下面增加限定条件来完成

可以看到计划的估算行数也从10变成了1,这是通过计算条件A = 1的选择率得到的

选择率 指的是 满足此条件的行数 / 总行数 = 此条件的选择率,上述用例中表a列数据为 1 - 10 则条件 a = 1 的选择率就是 1 / 10 = 0.1,用条件选择率 * 表估算行数,就得到了表经过条件过滤后的估算行数

-- 现在为查询加上count(*)函数
explain select count(*) from t1 where a = 1;
                       QUERY PLAN
---------------------------------------------------------
 Aggregate  (cost=7.13..7.13 rows=1 width=0)
   ->  Seq Scan on T1  (cost=0.00..7.13 rows=1 width=0)
         Scan Key: (A = 1)
(3 rows)

为SQL的目标列添加聚集函数来统计输出行数,从计划上可以看到通过添加 Aggregate (聚集算子)来完成, Seq Scan (顺序扫描算子)前添加了箭头并缩进在了Aggregate下面。

这说明这两个算子之间有个调用的关系,是上层的Aggregate调用下层的Seq Scan,也可以理解为,Aggregate运行所需的数据来自于Seq Scan。

注解

  • 一个算子可能调用多个子算子,他们拥有相同的缩进
-- 现在为查询增加一个连接表T2(数据1,2,3)
explain select count(*) from t1,t2 where a = 1;
                            QUERY PLAN
------------------------------------------------------------------
 Aggregate  (cost=14.20..14.20 rows=1 width=0)
   ->  Nested Loop(Inner Join)  (cost=0.00..14.19 rows=3 width=0)
         ->  Seq Scan on T1  (cost=0.00..7.13 rows=1 width=0)
               Scan Key: (A = 1)
         ->  Seq Scan on T2  (cost=0.00..7.03 rows=3 width=0)
(5 rows)

为SQL添加一个连接表,从SQL语义上看聚集的对象不再是T1表的数据,而是T1和T2连接后的数据,所以从计划上可以看到Aggregate的子算子不再是是Seq Scan而是 Nested Loop (连接算子)

Nested Loop调用Seq Scan获取T1,T2的数据后将数据连接起来,返回给Aggregate算子,再由Aggregate算子计算聚集函数后返回给用户

注解

  • 执行计划的结构是由算子组成的算子树,通常算子只有左子树和右子树,也可能有多个子树(比如 Append 算子)。
  • 叶节点是扫描算子,即数据的来源。