14.1. 使用EXPLAIN

113 阅读16分钟

14.1. 使用EXPLAIN

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

本节中的示例是在执行 9.3 开发源后从回归测试数据库中提取的。如果您自己尝试这些示例,您应该能够获得类似的结果,但您的估计成本和行计数可能会略有不同,因为 的统计信息是随机样本而不是精确样本,并且成本本质上在某种程度上取决于平台。VACUUM ANALYZE``ANALYZE

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

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

  • 估计的启动成本。这是输出阶段开始之前花费的时间,例如,在排序节点中进行排序的时间。
  • 估计的总成本。这是在假设计划节点运行到完成的情况下声明的,即检索所有可用行。实际上,节点的父节点可能无法读取所有可用行(请参阅下面的示例)。LIMIT
  • 此计划节点输出的估计行数。同样,假定节点已运行完成。
  • 此计划节点输出的行的估计平均宽度(以字节为单位)。

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

请务必了解,上层节点的成本包括其所有子节点的成本。同样重要的是要认识到成本只反映了计划者关心的事情。特别是,成本不考虑将结果行传输到客户端所花费的时间,这可能是实际运行时间中的一个重要因素;但是计划者忽略了它,因为它不能通过改变计划来改变它。(我们相信,每个正确的计划都会输出相同的行集。

该值有点棘手,因为它不是计划节点处理或扫描的行数,而是节点发出的行数。这通常小于扫描的数量,这是按节点上应用的任何 -clause 条件进行筛选的结果。理想情况下,顶级行估计值将近似于查询实际返回、更新或删除的行数。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';

您会发现它有 358 个磁盘页和 10000 行。估计成本的计算公式为(读取的磁盘页 * seq_page_cost)+(扫描的行数 * cpu_tuple_cost)。默认情况下,为 1.0 且为 0.01,因此估计成本为 (358 * 1.0) + (10000 * 0.01) = 458。tenk1``seq_page_cost``cpu_tuple_cost

现在,让我们修改查询以添加条件: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)

请注意,输出显示子句作为附加到 Seq Scan 计划节点的“筛选器”条件应用。这意味着计划节点会检查它扫描的每一行的条件,并仅输出通过条件的行。由于子句,输出行的估计值已减少。但是,扫描仍然必须访问所有 10000 行,因此成本并没有降低;实际上,它已经上升了一点(确切地说,增加了10000 * cpu_operator_cost),以反映检查条件所花费的额外CPU时间。EXPLAIN``WHERE``WHERE``WHERE

此查询将选择的实际行数为 7000,但估计值仅为近似值。如果您尝试复制此实验,您可能会得到略有不同的估计值;此外,它可以在每个命令后更改,因为生成的统计信息取自表的随机样本。rows``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

如果 中引用的多个列上有单独的索引,则计划人员可能会选择使用索引的 AND 或 OR 组合:WHERE

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 无法避免支付位图扫描的启动成本,因此使用这种方法的总成本将超过 25 个单位。LIMIT

让我们尝试使用我们一直在讨论的列连接两个表:

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)

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

在此示例中,连接的输出行计数与两次扫描的行计数的乘积相同,但并非在所有情况下都是如此,因为可能还有其他子句提及这两个表,因此只能在连接点应用,而不能应用于任一输入扫描。下面是一个示例: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

处理外部联接时,您可能会看到同时附加了“联接筛选器”和普通“筛选器”条件的联接计划节点。联接筛选器条件来自外部联接的子句,因此未通过联接筛选条件的行仍可能作为空扩展行发出。但是,在外部联接规则之后应用了纯筛选器条件,因此可以无条件地删除行。在内部联接中,这些类型的筛选器之间没有语义差异。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)

在这里,计划程序选择使用哈希连接,其中将一个表的行输入到内存中的哈希表中,然后扫描另一个表并探测哈希表是否与每行匹配。再次注意缩进如何反映计划结构:位图扫描是 Hash 节点的输入,该节点构造哈希表。然后返回到“哈希联接”节点,该节点从其外部子计划中读取行,并在哈希表中搜索每个行。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

查看变体计划的一种方法是强制计划者忽略它认为最便宜的任何策略,使用第 19.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)

这表明规划人员认为按索引扫描排序比顺序扫描和排序昂贵约 12%。当然,下一个问题是它是否正确。我们可以使用 来调查这一点,如下所述。onek``EXPLAIN ANALYZE

14.1.2.EXPLAIN ANALYZE

可以使用 选项检查计划员估计的准确性。使用此选项, 实际执行查询,然后显示每个计划节点中累积的真实行计数和真实运行时间,以及普通显示的相同估计值。例如,我们可能会得到这样的结果:EXPLAIN``ANALYZE``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

在某些查询计划中,子计划节点可能会多次执行。例如,内部索引扫描将在上述嵌套循环计划中每外行执行一次。在这种情况下,该值报告节点的执行总数,显示的实际时间和行值是每次执行的平均值。这样做是为了使数字与成本估算的显示方式具有可比性。乘以该值,得到在节点中实际花费的总时间。在上面的示例中,我们总共花费了 0.220 毫秒在 上执行索引扫描。loops``loops``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

“排序”节点显示使用的排序方法(特别是排序是在内存中还是在磁盘上)以及所需的内存或磁盘空间量。哈希节点显示哈希桶和批次的数量以及用于哈希表的峰值内存量。(如果批数超过 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
 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

规划器认为(非常正确)此示例表太小而无法进行索引扫描,因此我们有一个普通的顺序扫描,其中所有行都被筛选条件拒绝。但是,如果我们强制使用索引扫描,则会看到:

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

提供的数字有助于确定查询的哪些部分是 I/O 密集型的。BUFFERS

请记住,由于实际运行查询,因此任何副作用都将照常发生,即使查询可能输出的任何结果都会被丢弃以打印数据。如果要在不更改表的情况下分析数据修改查询,则可以在之后回滚命令,例如: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;

如本示例所示,当查询是 、 或命令时,应用表更改的实际工作由顶级“插入”、“更新”或“删除”计划节点完成。此节点下的计划节点执行查找旧行和/或计算新数据的工作。因此,在上面,我们看到了与已经看到的相同类型的位图表扫描,其输出被馈送到存储更新行的 Update 节点。值得注意的是,尽管数据修改节点可能需要相当长的运行时间(在这里,它消耗了大部分时间),但计划器目前不会在成本估算中添加任何内容来说明这项工作。这是因为每个正确的查询计划要完成的工作都是相同的,因此不会影响规划决策。INSERT``UPDATE``DELETE

当 or 命令影响继承层次结构时,输出可能如下所示:UPDATE``DELETE

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 节点需要考虑三个子表以及最初提到的父表。因此,有四个输入扫描子计划,每个表一个。为清楚起见,对“更新”节点进行了批注,以显示将要更新的特定目标表,顺序与相应的子计划相同。(这些注释是PostgreSQL 9.5的新功能;在以前的版本中,读者必须通过检查子计划来直观地了解目标表。

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

显示者包括执行程序启动和关闭时间,以及运行触发的任何触发器的时间,但不包括解析、重写或计划时间。执行触发器所花费的时间(如果有)包含在相关“插入”、“更新”或“删除”节点的时间中;但是执行触发器所花费的时间不计算在那里,因为触发器是在整个计划完成后触发的。在每个触发器(或)中花费的总时间也单独显示。请注意,延迟约束触发器在事务结束之前不会执行,因此 根本不考虑。Execution time``EXPLAIN ANALYZE``BEFORE``AFTER``AFTER``BEFORE``AFTER``EXPLAIN ANALYZE

14.1.3. 注意事项

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

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 节点始终将其实际行计数报告为零。

通常,将显示计划器创建的每个计划节点。但是,在某些情况下,执行程序可以根据计划时不可用的参数值确定某些节点不需要执行,因为它们无法生成任何行。(目前,这只能对正在扫描分区表的追加节点的子节点发生。发生这种情况时,这些计划节点将从输出中省略,而是显示注释。EXPLAIN``EXPLAIN``Subplans Removed: N