Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16

14.1. 使用EXPLAIN

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

本节中的例子是使用9.3 开发源代码执行VACUUM ANALYZE 之后从回归测试数据库中提取出来的。 如果你尝试自己的例子,你应该可以得到类似结果,但是你的估计代价和行计数可能会略有不同, 因为ANALYZE的统计信息是随机采样而不是精确值, 并且因为成本本质上是平台相关的。

这些例子使用EXPLAIN的默认"文本"输出格式, 这种格式紧凑并且便于人类阅读。如果你想把EXPLAIN 的输出交给程序做进一步分析,你应该使用它的某种机器可读的输出格式(XML、JSON 或 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子句被当做一个"过滤器" 条件附加到顺序扫描计划节点。这意味着该计划节点为它扫描的每一行检查该条件, 并且只输出通过该条件的行。因为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)

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

现在让我们给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 < 10用在了那个节点上。 t1.unique2 = t2.unique2子句现在还不相关,因此它不影响外层扫描的行计数。 嵌套循环连接节点将为从外层子节点得到的每一行运行它的第二个(或"内层")子节点。 当前外层行的列值可以被插入到内层扫描。这里,来自外层行的t1.unique2值是可用的, 所以我们得到一个计划,其开销与前面见到的简单SELECT ... WHERE t2.unique2 = constant 情况相似(估计的开销实际上比前面看到的略低,是因为在t2 上的重复索引扫描会利用到高速缓存)。然后,以外层扫描的开销为基础设置循环节点的开销, 加上对每个外层行的一个重复的内层扫描(10 * 7.87),再加上用于连接处理的一点 CPU 时间。

在这个例子里,连接的输出行计数等于两个扫描的行计数的乘积, 但通常并不是所有的情况中都如此,因为可能会有同时提及两个表的额外WHERE子句, 并且因此它只能应用于连接点,而不能是任何一个输入扫描。这里是一个例子:

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节点在读取数据时将它保存在内存中,然后在每一次后续执行时从内存返回数据。

在处理外连接时,你可能会看到连接计划节点同时附加有"连接过滤器" 和普通"过滤器"条件。连接过滤器条件来自于外连接的ON子句, 因此一个无法通过连接过滤器条件的行也能够作为一个空值扩展的行被发出。 但是一个普通过滤器条件在外连接规则之后应用并且因此无条件地移除行。 在一个内连接中这两种过滤器类型没有语义区别。

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

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显示的估计值。例如,我们可能得到这样一个结果:

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)
 Planning time: 0.181 ms
 Execution time: 0.501 ms

注意"actual time"值是以毫秒计的真实时间,而cost 估计值则以任意的单位表示,因此它们不大可能匹配上。 在这里面要查看的最重要的一点是估计的行计数是否合理地接近实际值。在这个例子中, 估计值都是完全正确的,但是在实际中非常少见。

在某些查询计划中,一个子规划节点很可能运行多次。例如, 内层索引扫描将在上述嵌套循环计划中的每一个外层行执行一次。 在这种情况下,loops值报告了该节点执行的总次数, 并且显示的实际时间和行数值是这些执行的平均值。 这么做的原因是让这些数字与开销估计显示的数字有可比性。将这些值乘上 loops值可以得到在该节点中实际消耗的总时间。在上面的例子中, 我们在执行tenk2的索引扫描上花费了总共 0.220 毫秒。

在某些情况中,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)
 Planning time: 0.194 ms
 Execution time: 8.008 ms

排序节点显示使用的排序方法(特别是,排序是在内存中还是磁盘上进行) 和需要的内存或磁盘空间量。哈希节点显示了哈希桶的数量和批数, 以及被哈希表所使用的内存量的峰值(如果批数超过一,也将会涉及到磁盘空间使用, 但是并没有被显示)。

另一种类型的额外信息是通过过滤器条件移除的行数:

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
 Planning time: 0.083 ms
 Execution time: 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
 Planning time: 0.040 ms
 Execution time: 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
 Planning time: 0.034 ms
 Execution time: 0.144 ms

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

EXPLAIN有一个BUFFERS选项可以和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
 Planning time: 0.088 ms
 Execution time: 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)
 Planning time: 0.079 ms
 Execution time: 14.727 ms

ROLLBACK;

正如在这个例子中所看到的,当查询是一个INSERTUPDATEDELETE命令时,应用表更改的实际工作由顶层插入、更新或删除计划节点完成。 这个节点之下的计划节点执行定位旧行和/或者计算新数据的工作。因此在上面, 我们看到我们已经见过的位图表扫描,并且其输出被传递给存储被更新行的更新节点。 值得注意的是,尽管数据修改节点可能花费大量的运行时间(这里,它消耗运行时间中的绝大部分), 规划器当前并没有对开销估计增加任何东西来说明这些工作。 这是因为这些工作对每一个正确的查询计划都得做,所以它不影响计划的选择。

UPDATEDELETE命令影响继承层次结构时,输出可能如下所示:

EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
                                    QUERY PLAN
-----------------------------------------------------------------------------------
 Update on parent  (cost=0.00..24.53 rows=4 width=14)
   Update on parent
   Update on child1
   Update on child2
   Update on child3
   ->  Seq Scan on parent  (cost=0.00..0.00 rows=1 width=14)
         Filter: (f1 = 101)
   ->  Index Scan using child1_f1_key on child1  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)
   ->  Index Scan using child2_f1_key on child2  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)
   ->  Index Scan using child3_f1_key on child3  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)

在此示例中,更新节点需要考虑三个子表以及最初提到的父表。因此, 有四个输入扫描子计划,每个表一个。为了清楚起见, 注释了更新节点以与相应子计划相同的顺序显示将被更新的特定目标表。 (这些注释是PostgreSQL 9.5中的新注释;在以前的版本中, 读者必须通过检查子计划直观地看到目标表)。

EXPLAIN ANALYZE显示的 Planning time是从一个已解析的查询生成查询计划并进行优化所花费的时间。 它并不包括解析或重写的时间。

EXPLAIN ANALYZE显示的Execution time 包括执行器启动和关闭的时间,以及运行被触发的任何触发器的时间,但是它不包括解析、 重写或规划的时间。如果有执行BEFORE触发器花费的时间, 它将被包括在相关的插入、更新或删除节点的时间内;但是用来执行AFTER 触发器的时间没有被计算,因为AFTER触发器是在整个计划完成后被触发的。 还单独显示每个触发器花费的总时间(BEFOREAFTER)。 注意延迟约束触发器将不会被执行,直到事务结束,并且因此根本不会被 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
 Planning time: 0.096 ms
 Execution time: 0.336 ms

索引扫描节点的估计成本和行计数显示为运行到完成。 但是实际上Limit节点在得到两行之后就停止请求行, 因此实际的行计数只有 2 并且运行时间低于开销估计所建议的时间。这并非估计错误, 这仅仅一种估计值和实际值显示方式上的不同。

合并连接中也有可能导致混淆的测量部分。合并连接将停止读取一个输入, 如果它用尽了其他输入,并且一个输入中的下一个键值大于另一个输入的最后一个键值; 在这种情况下,不会有更多的匹配,所以不需要扫描第一个输入的剩余部分。 这会导致不读取一个子节点的所有内容,其结果就像在LIMIT中所提到的。 另外,如果外层(第一个)子节点包含键值重复的行,内层(第二个) 子节点被备份并重新扫描其行中与该键值匹配的部分。EXPLAIN ANALYZE 会统计相同内层行的重复发出,就好像它们是真实的额外行。当有很多外层节点重复时, 对内层子计划节点所报告的实际行计数会显著地大于在内层关系中的实际行数。

由于实现的限制,BitmapAnd 和 BitmapOr 节点总是报告它们的实际行计数为零。