PostgresSQL-EXPLAIN

13.1. 使用 EXPLAIN

PostgreSQL 为给它的每一个查询产生一个查询规划。 为匹配查询结构和数据属性选择正确的规划对性能绝对有关键性的影响。 所以系统包含了一个复杂的规划器用于寻找最优的规划。 你可使用 EXPLAIN 命令察看规划器为每一个查询生成的查询规划是什么。 阅读查询规划是一门值得写一个至关长的教程的学问, 而我这份文档可不是这样的教程,可是这里有一些基本的信息。php

查询规划的结构是一个规划节点的树。 最底层的节点是表扫描节点:它们从表中返回原始数据行。 不一样的表访问模式有不一样的扫描节点类型:顺序扫描,索引扫描,以及位图索引扫描。 若是查询须要链接,汇集,排序,或者是对原始行的其它操做, 那么就会在扫描节点"之上"有其它额外的节点。 而且,作这些操做一般都有多种方法,所以在这些位置也有可能出现不一样的节点类型。 EXPLAIN 的输出给规划树里面的每一个节点都有一行输出, 显示基本的节点类型和规划器为执行这个规划节点计算出来的预计的开销值。 第一行(最上层的节点)是对该规划的总的执行开销的预计;这个数值就是规划器试图最小化的数值。html

这里是一个简单的例子,只是用来显示输出会有些啥: [1]sql

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  ()

EXPLAIN 引用的数据是:工具

  • 预计的启动开销(在输出扫描开始以前消耗的时间,也就是,在一个排序节点里作排续的时间)。oop

  • 预计的总开销(若是全部的行都被检索的话, 不过极可能不是这样:好比带有 LIMIT 子句的查询将会在 Limit 规划节点的输入节点里很快中止。)。性能

  • 预计的这个规划节点输出的行数。 (一样,只执行到完成为止)。测试

  • 预计的这个规划节点的行的平均宽度(以字节计算)。spa

开销是以磁盘页面的存取为单位计算的。也就是,定义上 1.0 等于一次顺序的磁盘页面抓取。 (同时也计算了 CPU 的开销;它们被用一些很是随意的捏造的权值被转换成磁盘页面单位。 若是你想试验这些东西,请参阅在 Section 17.6.2 里的运行时参数列表。)code

有一点很重要:那就是一个上层节点的开销包括它的全部子节点的开销。 还有一点也很重要:就是这个开销只反映规划器关心的东西。 尤为是开销没有把结果行传递给客户端的时间考虑进去, 这个时间可能在真正的总时间里面占据至关重要的份量; 可是被规划器忽略了,由于它没法经过修改规划来改变之。 (咱们相信,每一个正确的规划都将输出一样的记录集。)orm

输出的行数有一些小技巧,由于它不是规划节点处理/扫描过的行数,一般会少一些, 反映对应用于此节点上的任意 WHERE 子句条件的选择性估计。 一般而言,顶层的行预计会接近于查询实际返回,更新,或删除的行数。

回到咱们的例子:

EXPLAIN SELECT * FROM tenk1;
                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

这个例子就象例子自己同样直接了当。若是你作一个

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

你会发现 tenk1 有 358 磁盘页面和 10000 行。 所以开销计算为 358 次页面读取,定义为每块 1.0, 加上 10000 * cpu_tuple_cost,一般是 0.01(用命令 SHOW cpu_tuple_cost 查看)。

如今让咱们修改查询并增长一个WHERE条件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7033 width=244)
   Filter: (unique1 < 7000)

请注意 EXPLAIN 输出显示 WHERE 子句看成一个 "filter" 应用; 这意味着规划节点为它扫描的每一行检查该条件,而且只输出经过条件的行。 预计的输出行数下降了,由于有WHERE子句。 不过,扫描仍将必须访问全部 10000 行,所以开销没有下降; 实际上它还增长了一些以反映检查WHERE条件的额外 CPU 时间。

这条查询实际选择的行数是 7000,可是预计的数目只是个大概。 若是你试图重复这个试验,那么你极可能获得有些不一样的预计; 还有,这个预计会在每次 ANALYZE 命令以后改变, 由于 ANALYZE 生成的统计是从该表中随机抽取的样本计算的。

把查询限制条件改得更严格:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=2.37..232.35 rows=106 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..2.37 rows=106 width=0)
         Index Cond: (unique1 < 100)

这里,规划器决定使用两步的规划:最底层的规划节点访问一个索引,找出匹配索引条件的行的位置, 而后上层规划节点真实地从表里面抓取出那些行。独立地抓取数据行比顺序地读取它们的开销高不少, 可是由于并不是全部表的页面都被访问了,这么作实际上仍然比一次顺序扫描开销要少。 (使用两层规划的缘由是由于上层规划节点把索引标识出来的行位置在读取它们以前按照物理位置排序, 这样能够最小化独立抓取的开销。节点名称里面提到的"位图(bitmap)"是进行排序的机制。

若是 WHERE 条件有足够的选择性,规划器可能会切换到一个"简单的"索引扫描规划:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 3;

                                  QUERY PLAN
------------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.00..10.00 rows=2 width=244)
   Index Cond: (unique1 < 3)

在这个例子理,表的数据行是以索引顺序抓取的,这样就令读取它们的开销更大, 可是这里的行少得可怜,所以额外的行位置的排序并不值得。你最多见的就是看到这种规划类型只抓取一行, 以及是那些要求一个 ORDER BY 条件匹配索引顺序的查询。

向WHERE子句里面增长另一个条件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 3 AND stringu1 = 'xxx';

                                  QUERY PLAN
------------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.00..10.01 rows=1 width=244)
   Index Cond: (unique1 < 3)
   Filter: (stringu1 = 'xxx'::name)

新增的条件 stringu1 = 'xxx' 减小了预计的输出行, 可是没有减小开销,由于咱们仍然须要访问相同的行。 请注意 stringu1 子句不能当作一个索引条件施用 (由于这个索引只是在unique1 列上有)。 它是当作一个从索引中检索出的行的过滤器来用的。 所以开销实际上略微增长了一些以反映这个额外的检查。

若是在 WHERE 里面使用的好几个字段上有索引, 那么规划器可能会使用索引的 AND 或者 OR 的组合:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;
 
                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=11.27..49.11 rows=11 width=244)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=11.27..11.27 rows=11 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..2.37 rows=106 width=0)
               Index Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..8.65 rows=1042 width=0)
               Index Cond: (unique2 > 9000)

可是这么作要求访问两个索引,所以与只使用一个索引,而把另一个条件只看成过滤器相比,这个方法未必是更优。 若是你改变涉及的范围,你会看到规划器相应地发生变化。

让咱们试着使用咱们上面讨论的字段链接两个表:

EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;
  
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=2.37..553.11 rows=106 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=2.37..232.35 rows=106 width=244)
         Recheck Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..2.37 rows=106 width=0)
               Index Cond: (unique1 < 100)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..3.01 rows=1 width=244)
         Index Cond: ("outer".unique2 = t2.unique2)

在这个嵌套循环里,外层的扫描是和咱们前面看到的一样的位图索引, 所以其开销和行计数是同样的,由于咱们在该节点上附加了 WHERE 子句 unique1 < 100。 这个时候 t1.unique2 = t2.unique2 子句尚未什么关系, 所以它不影响外层扫描的行计数。对于内层扫描,当前外层扫描的数据行的 unique2 被插入内层索引扫描生成相似 t2.unique2 = constant 这样的索引条件。所以,咱们拿到和从 EXPLAIN SELECT * FROM tenk2 WHERE unique2 = 42 那边拿到的同样的内层扫描计划和开销。而后,之外层扫描的开销为基础设置循环节点的开销, 加上每一个外层行的一个重复(这里是 106 * 3.01),而后再加上链接处理须要的一点点 CPU 时间。

在这个例子里,链接的输出行数与两个扫描的行数的乘积相同, 可是一般并非这样的,由于一般你会有说起两个表的WHERE子句, 所以它只能应用于链接(join)点,而不能影响两个关系的输入扫描。 好比,若是咱们加一条 WHERE ... AND t1.hundred < t2.hundred, 将减小输出行数,可是不改变任何一个输入扫描。

寻找另一个规划的方法是经过设置每种规划类型的容许/禁止开关, (在 Section 17.6.1 里描述) 强制规划器抛弃它认为优秀的(扫描)策略。 (这个工具目前比较原始,但颇有用。又见Section 13.3。)

SET enable_nestloop = off;
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;
  
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Hash Join  (cost=232.61..741.67 rows=106 width=488)
   Hash Cond: ("outer".unique2 = "inner".unique2)
   ->  Seq Scan on tenk2 t2  (cost=0.00..458.00 rows=10000 width=244)
   ->  Hash  (cost=232.35..232.35 rows=106 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=2.37..232.35 rows=106 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..2.37 rows=106 width=0)
                     Index Cond: (unique1 < 100)

这个规划仍然试图用一样的索引扫描从tenk1 里面取出感兴趣的 100 行, 把它们藏在一个在内存里的散列(哈希)表里,而后对 tenk2 作一次顺序扫描,对每一条tenk2记录检测上面的散列(哈希)表, 寻找可能匹配t1.unique2 = t2.unique2 的行。 读取tenk1和创建散列表是此散列联接的所有启动开销, 由于咱们在开始读取tenk2 以前不可能得到任何输出行。 这个联接的总的预计时间一样还包括至关重的检测散列(哈希)表 10000 次的 CPU 时间。不过,请注意,咱们须要对 232.35 乘 10000; 散列(哈希)表的在这个规划类型中只须要设置一次

咱们能够用EXPLAIN ANALYZE检查规划器的估计值的准确性。 这个命令实际上执行该查询而后显示每一个规划节点内实际运行时间的和以及单纯EXPLAIN显示的估计开销。 好比,咱们能够象下面这样获取一个结果:

EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;
  
                                                            QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=2.37..553.11 rows=106 width=488) (actual time=1.392..12.700 rows=100 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=2.37..232.35 rows=106 width=244) (actual time=0.878..2.367 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..2.37 rows=106 width=0) (actual time=0.546..0.546 rows=100 loops=1)
               Index Cond: (unique1 < 100)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..3.01 rows=1 width=244) (actual time=0.067..0.078 rows=1 loops=100)
         Index Cond: ("outer".unique2 = t2.unique2)
 Total runtime: 14.452 ms

请注意 "actual time" 数值是以真实时间的毫秒计的, 而 "cost" 估计值是以任意磁盘抓取的单元计的; 所以它们极可能不一致。咱们要关心的事是两组比值是否一致。

在一些查询规划里,一个子规划节点极可能运行屡次。 好比,在上面的嵌套循环的规划里,内层的索引扫描是对每一个外层行执行一次的。 在这种状况下,"loops" 报告该节点执行的总数目, 而显示的实际时间和行数目是每次执行的平均值。 这么作的缘由是令这些数字与开销预计显示的数字具备可比性。 要乘以 "loops" 值才能得到在该节点时间花费的总时间。

EXPLAIN ANALYZE 显示的 "Total runtime" 包括执行器启动和关闭的时间, 以及花在处理结果行上的时间。它不包括分析,重写,或者规划的时间。 对于SELECT查询,总运行时间一般只是比从顶层规划节点汇报出来的总时间略微大些。 对于INSERT,UPDATE,和 DELETE 查询, 总运行时间可能会显著增大,由于它包括花费在处理结果行上的时间。 在这些查询里,顶层规划节点的时间其实是花在计算新行和/或定位旧行上的时间,可是不包括花在标记变化上的时间。

若是EXPLAIN的结果除了在你实际测试的状况以外不能推导出其它的状况, 那它就什么用都没有;好比,在一个小得象玩具的表上的结果不能适用于大表。 规划器的开销计算不是线性的,所以它极可能对大些或者小些的表选择不一样的规划。 一个极端的例子是一个只占据一个磁盘页面的表,在这样的表上,无论它有没有索引可使用, 你几乎都老是获得顺序扫描规划。规划器知道无论在任何状况下它都要进行一个磁盘页面的读取, 因此再扩大几个磁盘页面读取以查找索引是没有意义的

本站公众号
   欢迎关注本站公众号,获取更多信息