14.1. 使用 EXPLAIN

PostgreSQL 为收到的每个查询设计一个“查询计划”。选择正确的计划以匹配查询结构和数据的属性对于获得良好的性能绝对至关重要,因此该系统包括一个复杂的* planner *,它试图选择良好的计划。您可以使用EXPLAIN命令查看计划者为任何查询创建的查询计划。计划阅读是一门艺术,需要一定的经验来掌握,但是本节尝试介绍基础知识。

本节中的示例是使用 9.3 开发源在进行VACUUM ANALYZE之后从回归测试数据库中提取的。如果您自己尝试这些示例,您应该能够获得相似的结果,但是您估计的成本和行数可能会略有不同,因为ANALYZE的统计信息是随机 samples 而不是准确的,并且成本本质上与平台有关。

这些示例使用EXPLAIN的默认“文本”输出格式,该格式紧凑且便于人类阅读。如果要将EXPLAIN的输出提供给程序进行进一步分析,则应改用其机器可读的输出格式之一(XML,JSON 或 YAML)。

14 .1.1. 讲解基础

查询计划的结构是计划节点的树。树底部的节点是扫描节点:它们从表中返回原始行。对于不同的表访问方法,有不同类型的扫描节点:Sequences 扫描,索引扫描和位图索引扫描。也有非表行源,例如VALUES子句和FROM中的集合返回函数,它们具有自己的扫描节点类型。如果查询需要对原始行进行联接,聚合,排序或其他操作,则扫描节点上方将有其他节点来执行这些操作。同样,通常有不止一种可能的方式来执行这些操作,因此不同的节点类型也会在此处出现。 EXPLAIN的输出对于计划树中的每个节点都有一行,显示基本节点类型以及计划者为执行该计划节点所做的成本估算。从节点的摘要行缩进可能会出现其他行,以显示节点的其他属性。第一行(最顶层节点的摘要行)具有该计划的估计总执行成本;计划者试图使该数字最小化。

这是一个简单的示例,只是为了显示输出内容:

EXPLAIN SELECT * FROM tenk1;

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

由于此查询没有WHERE子句,因此它必须扫描表的所有行,因此计划者选择使用简单的 Sequences 扫描计划。括号中的数字是(从左到右):

  • 估计的启动成本。这是输出阶段可以开始之前花费的时间,例如,在排序节点中进行排序的时间。

  • 估计总费用。这是在假设计划节点已运行完毕(即检索到所有可用行)的假设下得出的。实际上,节点的父节点可能会停止读取所有可用行(请参见下面的LIMIT示例)。

  • 此计划节点输出的估计行数。同样,假定该节点运行完毕。

  • 此计划节点输出的行的估计平均宽度(以字节为单位)。

成本以由计划者的成本参数确定的任意单位进行度量(请参见Section 19.7.2)。传统做法是以磁盘页面提取为单位来衡量成本。也就是说,通常将seq_page_cost设置为1.0,并相对于此设置其他成本参数。本节中的示例使用默认的 cost 参数运行。

了解上级节点的成本包括其所有子节点的成本非常重要。同样重要的是要意识到,成本仅反映计划者关心的事情。特别是,成本不考虑将结果行传输给 Client 端所花费的时间,这可能是实际经过时间的重要因素;但是计划者会忽略它,因为它无法通过更改计划来更改它。 (我们相信,每个正确的计划都会输出相同的行集.)

rows值有点棘手,因为它不是计划节点处理或扫描的行数,而是该节点发出的行数。由于被节点上应用的任何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子句被应用为附加到 Seq 扫描计划节点的“过滤器”条件。这意味着计划节点检查其扫描的每一行的条件,并仅输出通过条件的行。由于WHERE子句,输出行的估计已减少。但是,扫描仍将必须访问所有 10000 行,因此成本并未降低。实际上,它已经提高了一点(准确地说是 10000 * cpu_operator_cost),以反映检查WHERE条件所花费的额外 CPU 时间。

该查询将选择的实际行数为 7000,但是rows估算值仅为近似值。如果您尝试重复此实验,则可能会得出略有不同的估算值;此外,它可以在每个ANALYZE命令之后更改,因为ANALYZE产生的统计信息来自表的随机 samples。

现在,让我们对条件进行更严格的限制:

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)

在这里,计划者决定使用两步计划:子计划节点访问索引以查找与索引条件匹配的行的位置,然后上层计划节点实际上从表本身获取这些行。单独获取行比 Sequences 读取要昂贵得多,但是由于并非必须访问表的所有页面,因此这比 Sequences 扫描便宜。 (使用两个计划级别的原因是,上层计划节点在读取之前将索引标识的行位置按物理 Sequences 排序,以最大程度地减少单独提取的开销.节点名称中提到的“位图”是一种机制,进行排序.)

现在让我们向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)

在这种类型的计划中,表行是按索引 Sequences 获取的,这使它们的读取更加昂贵,但由于行数太少,因此不值得对行位置进行排序所产生的额外成本。对于只会读取一行的查询,您通常会看到这种计划类型。它也常用于具有与索引 Sequences 匹配的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)

在此计划中,我们有一个嵌套循环联接节点,其中两个表扫描作为 Importing 或子级。节点摘要线的缩进反映了计划树的结构。联接的第一个(或“外部”)子级是位图扫描,与我们之前看到的类似。它的成本和行数与从SELECT ... WHERE unique1 < 10获得的成本和行数相同,因为我们在该节点上应用了WHERE子句unique1 < 10t1.unique2 = t2.unique2子句尚不相关,因此它不会影响外部扫描的行数。嵌套循环连接节点将对从外部子级获得的每一行运行其第二个子级或“内部”子级。当前外部行中的列值可以插入内部扫描中。在这里,外排的t1.unique2值可用,因此我们获得了一个计划和成本,与上面针对简单SELECT ... WHERE t2.unique2 = constant情况所见的相似。 (由于预计在t2上进行重复索引扫描期间会发生缓存,因此估计的成本实际上比上面看到的要低.)然后,根据节点的成本来设置循环节点的成本。外部扫描,再加上每个外部行的内部扫描重复一次(此处为 10 * 7.91),再加上少量的 CPU 时间用于连接处理。

在此示例中,联接的输出行数与两次扫描的行数的乘积相同,但并非在所有情况下都是如此,因为可能会有另外的WHERE子句同时提及两个表,因此只能在联接点应用,而不要 Importing 扫描。这是一个例子:

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索引中进行测试,因此它在连接节点上应用。这样可以减少连接节点的估计输出行数,但不会更改任何 Importing 扫描。

请注意,这里的计划者已选择通过将“实现”计划节点放在连接的内部来“实现”内部关系。这意味着t2索引扫描将只执行一次,即使嵌套循环联接节点需要读取该数据十次,对于外部关系的每一行一次。 Materialize 节点在读取数据时将其保存在内存中,然后在每次后续遍历时从内存中返回数据。

在处理外部联接时,您可能会看到同时附有“联接过滤器”和普通“过滤器”条件的联接计划节点。联接过滤器条件来自外部联接的ON子句,因此未通过联接过滤器条件的行仍可能作为空扩展行发出。但是在外部联接规则之后应用了普通的 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)

在这里,计划者选择使用哈希联接,其中将一个表的行 Importing 到内存中的哈希表中,然后扫描另一张表,并探查哈希表以查找与每一行的匹配情况。再次注意缩进如何反映计划结构:在tenk1上进行位图扫描是哈希节点的 Importing,哈希节点构造哈希表。然后返回到“哈希联接”节点,该节点从其外部子计划中读取行并在哈希表中搜索每个行。

联接的另一种可能类型是合并联接,如下所示:

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)

合并联接要求其 Importing 数据在联接键上进行排序。在此计划中,通过使用索引扫描对tenk1数据进行排序以按正确的 Sequences 访问行,但是对于onek来说,优选 Sequences 扫描和排序,因为该表中还有许多行要访问。 (由于索引扫描需要非 Sequences 的磁盘访问,因此 Sequences 扫描和排序经常胜过索引扫描以对许多行进行排序.)

查看变体计划的一种方法是,使用Section 19.7.1中描述的启用/禁用标志,迫使计划者忽略其认为最便宜的任何策略。 (这是一个粗略的工具,但很有用.另请参阅Section 14.3。)例如,如果我们不相信 Sequences 扫描和排序是处理上一个示例中表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进行排序比 Sequences 扫描和排序的费用高出约 12%。当然,下一个问题是这是否正确。我们可以使用EXPLAIN ANALYZE进行调查,如下所述。

14 .1.2. 解释分析

使用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

请注意,“实际时间”值以毫秒为单位,而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)
 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

这些计数对于连接节点上应用的过滤条件特别有价值。仅当过滤条件拒绝至少一个已扫描的行或在连接节点的情况下可能的连接对时,“行已删除”行才会出现。

“有损”索引扫描会发生类似于过滤条件的情况。例如,考虑以下搜索包含特定点的多边形:

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

计划者认为(非常正确)该 samples 表太小而无法进行索引扫描,因此我们进行了普通的 Sequences 扫描,其中所有行都被过滤条件拒绝。但是,如果我们强制使用索引扫描,则会看到:

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命令时,应用表更改的实际工作是由顶级“插入”,“更新”或“删除”计划节点完成的。在该节点下面的计划节点执行定位旧行和/或计算新数据的工作。因此,在上面,我们看到了已经看到的相同类型的位图表扫描,并且其输出被馈送到存储更新行的 Update 节点。值得注意的是,尽管数据修改节点会花费大量的运行时间(在这里,这消耗了大部分时间),但是计划人员目前并未在成本估算中添加任何内容来说明这项工作。这是因为每个正确的查询计划要完成的工作都是相同的,因此不会影响计划决策。

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)

在此示例中,Update 节点需要考虑三个子表以及最初提到的父表。因此,有四个 Importing 扫描子计划,每个表一个。为了清楚起见,对“更新”节点进行了 Comments,以与相应子计划相同的 Sequences 显示将要更新的特定目标表。 (这些 Comments 是 PostgreSQL 9.5 以来的新增功能;在以前的版本中,Reader 必须通过检查子计划来了解目标表.)

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

EXPLAIN ANALYZE所示的Execution time包括执行程序的启动和关闭时间,以及运行任何已触发的触发器的时间,但不包括解析,重写或计划时间。相关的“插入”,“更新”或“删除”节点的时间中包括执行BEFORE触发器所花费的时间(如果有)。但是执行AFTER触发器所花费的时间并未计入其中,因为在整个计划完成后会触发AFTER触发器。每个触发器(BEFOREAFTER)所花费的总时间也分别显示。注意,延迟约束触发器直到事务结束才执行,因此EXPLAIN ANALYZE完全不考虑。

14.1.3. Caveats

EXPLAIN ANALYZE度量的运行时间有两种重要的方式可以偏离同一查询的正常执行。首先,由于没有输出行交付给 Client 端,因此不包括网络传输成本和 I/O 转换成本。其次,EXPLAIN ANALYZE所增加的测量开销可能很大,尤其是在gettimeofday()os 调用较慢的计算机上。您可以使用pg_test_timing工具来衡量系统计时的开销。

EXPLAIN结果不应外推到与您实际测试的情况大不相同的情况;例如,不能假定玩具大小桌子上的结果适用于大桌子。计划者的成本估算不是线性的,因此它可能为更大或更小的表选择其他计划。一个极端的例子是,在仅占用一个磁盘页面的表上,无论索引是否可用,您几乎总是会得到一个 Sequences 扫描计划。计划者意识到,无论如何都要读取一个磁盘页面来处理该表,因此扩展其他页面读取以查看索引没有任何价值。 (我们在上面的polygon_tbl示例中看到了这种情况.)

在某些情况下,实际值和估计值不能很好地匹配,但是没有什么是 true 错误的。当由于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,并且运行时间少于成本估算所建议的时间。这不是估计错误,只是显示估计和真实值的方式上的差异。

合并联接还具有度量工件,可能会使粗心的人感到困惑。如果合并联接用尽了另一个 Importing 并且一个 Importing 中的下一个键值大于另一个 Importing 的最后一个键值,则它将停止读取该 Importing。在这种情况下,将不再有匹配项,因此无需扫描其余的第一个 Importing。这导致不读取一个孩子的全部,结果类似于LIMIT所述。另外,如果外部(第一个)子项包含具有重复键值的行,则将备份内部(第二个)子项并对其与该键值匹配的行部分进行重新扫描。 EXPLAIN ANALYZE计算相同内部行的这些重复运行,就好像它们是真实的其他行一样。当有许多外部重复项时,内部子计划节点的报告的实际行数可能大大大于内部关系中实际存在的行数。

由于实现限制,BitmapAnd 和 BitmapOr 节点始终将其实际行计数报告为零。