14.1. 使用EXPLAIN

PostgreSQL对每个查询产生一个查询规划。 为匹配查询结构和数据属性选择正确的规划对性能绝对有关键性的影响。 因此系统包含了一个复杂的规划器用于寻找最优的规划。 你可以使用EXPLAIN命令察看规划器为每个查询生成的查询规划是什么。 阅读查询规划是一门需要掌握一些经验的艺术,但是这一节试图覆盖其中的基础。

本节的例子是从数据库执行VACUUM ANALYZE之后的回归测试中提取的,使用9.3开发源。 如果你尝试自己的例子,你应该可以得到类似结果,但你的估计成本及行数可能会略有不同,因为 ANALYZE的统计数据是随机样本,而不是确切的,并且因为成本本身有点依赖于平台。

该示例使用EXPLAIN的缺省"文本"输出格式, 它结构紧凑,便于人们阅读。如果你想提供EXPLAIN输出给程序用作进一步分析, 你应该使用它的机器可读的输出格式之一(XML, JSON, or YAML)来代替。

14.1.1. EXPLAIN基础

查询规划的结构是一个规划节点的树。最底层的节点是表扫描节点: 它们从表中返回原始数据行。不同的表访问模式有不同的扫描节点类型: 顺序扫描、索引扫描、位图索引扫描。 也有非表行来源,如VALUES子句和FROM中返回行集的函数, 它们有自己的扫描节点类型。 如果查询需要连接、聚合、排序、或者对原始行的其它操作, 那么就会在扫描节点之上有其它额外的节点。并且,做这些操作通常都有多种方法, 因此在这些位置也有可能出现不同的节点类型。EXPLAIN给规划树中每个节点都输出一行, 显示基本的节点类型和规划器为执行这个规划节点预计的开销值。 可能会出现附加的行,这些行从节点的汇总行缩进,显示这个节点的额外属性。 第一行(最上层的汇总行节点)是对该规划的总执行开销的预计;这个数值就是规划器试图最小化的数值。

这里是一个简单的例子,只是用来显示输出会有些什么内容:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

由于此查询没有WHERE子句,它必须扫描所有表的行,所以规划器已经选择使用一个简单的顺序扫描计划。 括号中引用的数值是(从左到右):

开销是用规划器的成本参数(参见节第 18.7.2 节)决定的任意的单位来衡量的。 习惯上以磁盘页面抓取为单位, 也就是seq_page_cost将被按照习惯设为1.0(一次顺序的磁盘页面抓取), 其它开销参数将参照它来设置。本节的例子都假定这些参数使用默认值。

有一点很重要:一个上层节点的开销包括它的所有子节点的开销。还有一点也很重要: 这个开销只反映规划器关心的东西,尤其是没有把结果行传递给客户端的时间考虑进去, 这个时间可能在实际的总时间里占据相当重要的分量,但是被规划器忽略了, 因为它无法通过修改规划来改变。(我们相信,每个正确的规划都将输出同样的记录集。)

值有一些小技巧,因为它不是规划节点处理或扫描过的行数, 而是节点输出的行数。通常会少于扫描数,正如应用于此节点上的任意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 行。 估计成本通过(磁盘页面读取*seq_page_cost)+(行扫描*cpu_tuple_cost)计算。默认情况下, seq_page_cost是1.0,cpu_tuple_cost是0.01, 因此估计成本为(358 * 1.0) + (10000 * 0.01) = 458。

现在让我们修改查询并增加一个WHERE条件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

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

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

这条查询实际选择的行数是7000 ,但是预计的行数只是个大概。如果你试图重复这个试验, 那么你很可能得到不同的预计。还有,这个预计会在每次ANALYZE命令之后改变, 因为ANALYZE生成的统计是从该表中随机抽取的样本计算的。

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

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
         Index Cond: (unique1 < 100)

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

现在让我们添加另外一个条件到WHERE子句:

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

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.04..229.43 rows=1 width=244)
   Recheck Cond: (unique1 < 100)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
         Index Cond: (unique1 < 100)

新增的条件stringu1 = 'xxx'减少了预计的输出行,但是没有减少开销, 因为我们仍然需要访问相同的行。 请注意,stringu1子句不能当做一个索引条件使用,因为这个索引只建立在unique1列上。 它被当做一个从索引中检索出的行的过滤器来使用。 因此开销实际上略微增加了一些以反映这个额外的检查。

在某些情况下规划器更加喜欢"简单的"索引扫描规划:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;

                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
   Index Cond: (unique1 = 42)

在这种规划类型中,表的数据行是以索引顺序抓取的,这样就令读取它们的开销更大, 但是这里的行少得可怜,因此对行位置的额外排序并不值得。最常见的就是看到这种规划类型只抓取一行, 以及那些要求ORDER BY条件匹配索引顺序的查询。因为那时候不需要多余的排序步骤去满足ORDER BY

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

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
               Index Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)
               Index Cond: (unique2 > 9000)

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

下面是一个例子,显示LIMIT的影响:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.48 rows=2 width=244)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..71.27 rows=10 width=244)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)

这是上面相同的查询,但我们增加了LIMIT,使得不是所有的行都需要被检索,因此规划器关于该怎么做也改变了主意。 请注意,被显示的索引扫描节点的总成本和行数就好像它是运行完毕的。然而,Limit节点预计在提取这些行的仅仅五分之一后停止, 所以其总成本只有五分之一之多,这就是这个查询实际的估算成本。该计划优于增加一个Limit节点到 先前的计划,因为该Limit无法避免支付位图扫描的启动成本,所以总成本将超过使用这种方法25个单位。

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

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.62 rows=10 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244)
         Index Cond: (unique2 = t1.unique2)

在这个规划中,我们有一个嵌套循环连接节点,它以两个表扫描作为输入或者子节点。 对节点汇总行的缩进反映了规划树的结构。 连接的第一个,或者"外层"子节点就是类似于我们之前看到的位图扫描。 其成本和行数是一样的,正如我们从SELECT ... WHERE unique1 < 10获得的。 因为我们只能在那个节点上应用WHERE 子句 unique1 < 10t1.unique2 = t2.unique2子句还没有任何关系,因此它不影响外层扫描的行计数。 嵌套循环连接节点将运行它的第二部分, 或者"内层"子节点,对从外层子节点获得的每一行运行一次。 从当前的外层行获得的列值可以被插入到内层扫描。这儿,从外层行中获得的t1.unique2是可用的。 这样我们就得到一个成本类似于我们上面看到的简单的SELECT ... WHERE t2.unique2 = constant的计划。 (估计成本实际上比上面看到的低一点,因为在t2上进行重复的索引扫描期间高速缓存会发生作用)。 然后,以外层扫描的开销为基础设置循环节点的开销,加上每个外层行的一个重复的内层扫描(这里是10 * 7.91), 再加上连接处理需要的一点点CPU时间。

在这个例子里,连接的输出行数与两个扫描的行数的乘积相同,但通常并不是这样的, 因为可能会有同时涉及两个表的WHERE子句,它只能应用于连接(join)点而不能是任何一个输入扫描。 这里有一个例子:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;

                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..49.46 rows=33 width=488)
   Join Filter: (t1.hundred < t2.hundred)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Materialize  (cost=0.29..8.51 rows=10 width=244)
         ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..8.46 rows=10 width=244)
               Index Cond: (unique2 < 10)

条件t1.hundred < t2.hundred不能 在tenk2_unique2索引中被测试,因此它被应用在 连接节点。这减少了连接节点的预计输出行数, 但不改变任何一个输入扫描。

注意,这里的规划器已经选择"物化"连接的内层表, 通过在上面放一个Materialize计划节点。这也就是说,只执行一次t2的索引扫描,尽管 嵌套循环连接节点需要读取十次那样的数据,对来自外层表的每一行执行一次。 Materialize节点在读取时将数据保存在内存中,然后对每个后续过程从内存中返回数据。

当处理外连接时,你可能会看到同时附属了"Join Filter"以及纯"Filter"条件的连接计划节点。 Join Filter条件来自于外连接的ON子句,因此 不满足Join Filter条件的行仍然可能作为一个NULL的扩展行(null-extended)被输出。 但一个纯的Filter条件在外连接规则之后被应用,因此无条件地删除行。 在内连接中这些过滤器类型之间没有语义差异。

如果我们改变查询的选择性,我们可能会得到一个非常不同的连接计划:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Hash Join  (cost=230.47..713.98 rows=101 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
   ->  Hash  (cost=229.20..229.20 rows=101 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
                     Index Cond: (unique1 < 100)

在这里,规划器选择使用一个哈希连接,表中的行被输入到内存中的哈希表中,在此之后,扫描其他 表并且为了每一行的匹配探测哈希表。 再次注意如何缩进来反映规划结构:在tenk1上的位图扫描是输入到哈希节点,它构造哈希表。 这之后返回到哈希连接节点,它从它的外层子计划中读取行并且为每一行搜索哈希表。

另一种可能的连接类型是合并连接,在这里说明:

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Merge Join  (cost=198.11..268.19 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)
         Filter: (unique1 < 100)
   ->  Sort  (cost=197.83..200.33 rows=1000 width=244)
         Sort Key: t2.unique2
         ->  Seq Scan on onek t2  (cost=0.00..148.00 rows=1000 width=244)

合并连接要求其输入的数据在连接键上进行排序。在这个 规划中tenk1数据是通过使用索引扫描访问正确顺序的行来进行排序的。 但顺序扫描和排序是onek的首选,因为在这个表上有很多行要被访问。 (对于大量行的排序,顺序扫描加排序经常打败索引扫描,因为索引扫描需要非连续的磁盘访问)

找另外一个规划的方法是通过设置每种规划类型的允许/禁止开关(在第 18.7.1 节里描述), 强迫规划器抛弃它认为优秀的(扫描)策略。这个工具目前比较原始,但很有用。又见第 14.3 节。 例如,如果我们不相信顺序扫描加排序对于前面例子中处理表onek是最好的方式,我们可以尝试

SET enable_sort = off;

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Merge Join  (cost=0.56..292.65 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)
         Filter: (unique1 < 100)
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..224.79 rows=1000 width=244)

这表明规划期认为通过索引扫描排序onek比顺序扫描和排序更昂贵约12%。 当然,接下来的问题是它是否是对的。我们可以使用EXPLAIN ANALYZE调查,正如下面所讨论的:

14.1.2. EXPLAIN ANALYZE

我们可以用EXPLAINANALYZE检查规划器的估计值的准确性。 这个命令实际上执行该查询,然后显示每个规划节点的实际行计数和实际运行时间,以及单纯的EXPLAIN显示的估计成本。 比如,我们可能会得到一个类似下面的结果:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244) (actual time=0.057..0.121 rows=10 loops=1)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.024..0.024 rows=10 loops=1)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244) (actual time=0.021..0.022 rows=1 loops=10)
         Index Cond: (unique2 = t1.unique2)
 Total runtime: 0.501 ms

请注意"actual time"数值是以真实时间的毫秒计的,而cost估计值则是以任意的单位; 因此它们很可能不一致。 通常最重要的事情是看是否估计行数相当接近于现实。在这个例子中, 估计都是完全正确的,但是实际上这是相当不寻常。

在一些查询规划里,一个子规划节点很可能运行多次。比如,在上面的嵌套循环的规划里, 内层的索引扫描对每个外层行执行一次。在这种情况下,loops报告该节点执行的总数目, 而显示的实际时间和行数目是每次执行的平均值。这么做的原因是令这些数字与开销预计显示的数字具有可比性。 要乘以loops值才能获得在该节点花费的总时间。在上面的例子中,我们共需要0.220毫秒来执行tenk2的索引扫描。

在某些情况下EXPLAIN ANALYZE还显示除了规划节点执行时间和行数以外的执行统计数据。 例如,排序和哈希节点提供额外的信息:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;

                                                                 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)
   Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 77kB
   ->  Hash Join  (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 rows=100 loops=1)
         Hash Cond: (t2.unique2 = t1.unique2)
         ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.007..2.583 rows=10000 loops=1)
         ->  Hash  (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 28kB
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244) (actual time=0.080..0.526 rows=100 loops=1)
                     Recheck Cond: (unique1 < 100)
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.049..0.049 rows=100 loops=1)
                           Index Cond: (unique1 < 100)
 Total runtime: 8.008 ms

排序节点显示使用的排序方法(特别是,排序是否在内存或磁盘上)以及所需的内存或磁盘空间量。 哈希节点显示哈希桶数量,批处理数以及用于哈希表的内存峰值数。 (如果批处理数大于1,还将涉及到磁盘空间使用情况,但是没有被显示。)

另一种类型的附加信息是通过过滤条件删除的行数:

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

                                               QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
 Total runtime: 5.905 ms

这些计数对于应用在连接节点的过滤条件特别有价值。 "Rows Removed"只出现在至少有一个被扫描的行,或者连接节点的情况下的潜在连接对, 被过滤条件拒绝的情况下。

类似于过滤条件的情况发生在"有损的"索引扫描上。 例如,考虑下面这个多边形包含一个指定点的搜索:

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Seq Scan on polygon_tbl  (cost=0.00..1.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 4
 Total runtime: 0.083 ms

规划器认为(很正确)这个取样表太小了不值得费心地使用索引扫描,所以我们有一个纯顺序扫描, 其中所有行通过过滤条件被拒绝。但是,如果我们强制 使用索引扫描,我们看到:

SET enable_seqscan TO off;

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=32) (actual time=0.062..0.062 rows=0 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
 Total runtime: 0.144 ms

在这里,我们可以看到,索引返回一个候选行,它 通过索引条件上的重新检查被拒绝。这是因为 GiST索引对于多边形包含测试是"有损的":它实际上 返回带有和目标重叠的多边形的行,然后我们在那些行上进行确切的包含测试。

EXPLAINBUFFERS选项,它可以和ANALYZE一起使用以获得更多的运行时统计数据:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Buffers: shared hit=15
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1)
         Buffers: shared hit=7
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
               Index Cond: (unique1 < 100)
               Buffers: shared hit=2
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.227..0.227 rows=999 loops=1)
               Index Cond: (unique2 > 9000)
               Buffers: shared hit=5
 Total runtime: 0.423 ms

BUFFERS提供的数值有助于识别查询的哪些部分是最I/O密集型的。

请记住,因为EXPLAIN ANALYZE实际运行查询, 即使支撑打印EXPLAIN数据的查询的可能输出被丢弃了,任何的副作用还是一样会发生。 如果要分析一个修改数据的查询而不改变你的表,你可以在之后回滚命令,例如:

BEGIN;

EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;

                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Update on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
               Index Cond: (unique1 < 100)
 Total runtime: 14.727 ms

ROLLBACK;

如该示例中,当查询是INSERT,UPDATE或者DELETE命令时, 实际应用表变更的工作是由顶层的插入、更新、或删除规划节点完成的。 这个节点下的规划节点进行定位旧的行和/或计算新数据。 所以上面,我们看到了在前面已经见到过的同样的位图扫描, 并且其输出被传递给存储被更新行的更新节点。 值得注意的是,虽然修改数据的节点可能花费大量的运行时间(在这里,它消耗了运行时间中的绝大大部分), 规划器当前不为这项工作添加任何东西到成本估算中。 这是因为对每一个正确的查询规划,即使做了这样的工作,结果也是一样的,因此它不影响规划的决定。

EXPLAIN ANALYZE显示的Total runtime包括执行器启动和关闭的时间, 以及被激发的任何触发器运行时间。但它不包括分析、重写、规划的时间。 执行BEFORE触发器花费的时间,如果有的话,被包含在相关的插入,更新或删除节点的时间里, 但执行AFTER触发器的时间花费并不计算在内, 因为整个规划完成之后,才触发AFTER触发器。 单独显示每个触发器花费的总时间(BEFORE 或者AFTER)。 需要注意的是延迟约束触发器直到事务结束将不会执行,因而不会通过EXPLAIN ANALYZE显示。

14.1.3. 警告

有两个值得注意的因素会导致 EXPLAIN ANALYZE测量出的运行时间可能偏离相同查询正常执行时所花费的时间。 首先,由于没有输出行被传递到客户端, 所以没有包含网络传输成本和I/O转换成本。 其次,通过EXPLAIN ANALYZE增加的测量开销可能是巨大的, 特别是在gettimeofday()操作系统调用很慢的机器上。你可以使用 pg_test_timing工具来测量你系统上的时间测量开销。

EXPLAIN的结果不应该被外推到和你实际测试的场景差别巨大的其它场景中。 比如,在一个小得像玩具的表上的结果不能适用于大表。规划器的开销计算不是线性的, 因此它很可能对大些或者小些的表选择不同的规划。一个极端的例子是一个只占据一个磁盘页面的表, 在这样的表上,不管它有没有索引可以使用,你几乎都总是得到顺序扫描规划。 规划器知道不管在任何情况下它都要进行一个磁盘页面的读取, 所以再花费额外的磁盘页面读取以查找索引是没有价值的。(我们已经从上面polygon_tbl的例子中看到这种情况了)

在某些情况下,实际值与估计值不能很好的匹配,但没有什么是真的错了。 比如,当规划节点的执行被LIMIT或类似效果的东西突然停止时,就会发生这样的情况。 。例如,我们以前曾经使用过的LIMIT查询。

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                                          QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.71 rows=2 width=244) (actual time=0.177..0.249 rows=2 loops=1)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..72.42 rows=10 width=244) (actual time=0.174..0.244 rows=2 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
 Total runtime: 0.336 ms

索引扫描节点的估计成本和行计数是按照它被运行完毕显示的。 但实际上,在获得了2行之后,Limit节点停止了行请求, 所以实际的行数只有2和,并且运行时间低于成本估算。 这不是估计错误,仅仅是估计值和真实值显示上的不符。

合并连接中也有可能导致混淆的测量部分。 合并连接将停止读取一个输入,如果它用尽了其他输入,并且输入端的下一个关键值大于其他输入的最后一个关键值; 在这种情况下,就不可能有更多的匹配,所以不需要扫描第一个输入的其余部分。 这会导致不会读完一个子节点所有的行,结果就像前面提到的LIMIT。 此外,如果外层(第一个)子节点包含键值重复的行,内层(第二个)子节点被后退并重新扫描以获得匹配这个键值的行。 EXPLAIN ANALYZE会计数同一内层行的重复发行,就像它们是真正的附加行。 当外层节点有许多重复键值,报告的内层节点的实际行数可能显著大于在内层节点上的实际行数。

由于实现上的限制,BitmapAnd和BitmapOr节点总是报告自己的实际行数为零。