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

14.1. 使用 EXPLAIN #

14.1.1. EXPLAIN 基础知识
14.1.2. EXPLAIN ANALYZE
14.1.3. 注意事项

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

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

这些示例使用 EXPLAIN 的默认 text 输出格式,该格式紧凑且便于人类阅读。如果您想将 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 子句,因此它必须扫描表的所有行,所以规划器选择使用简单的顺序扫描计划。括号中引用的数字(从左到右)是

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

  • 估计总成本。这是在假设计划节点运行到完成的情况下陈述的,即检索所有可用行。在实践中,节点的父节点可能会在读取所有可用行之前停止(请参见下面的 LIMIT 示例)。

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

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

成本以规划器的成本参数确定的任意单位进行衡量(请参见 第 20.7.2 节)。传统做法是以磁盘页面获取单位衡量成本;也就是说,通常将 seq_page_cost 设置为 1.0,并将其他成本参数相对于该值进行设置。本节中的示例使用默认成本参数运行。

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

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 Scan 计划节点的 filter 条件应用。这意味着计划节点检查它扫描的每一行的条件,并且仅输出通过条件的行。由于 WHERE 子句,输出行的估计值已减少。但是,扫描仍然必须访问所有 10000 行,因此成本并未降低;事实上,它略有上升(确切地说,上升了 10000 * cpu_operator_cost),以反映检查 WHERE 条件所花费的额外 CPU 时间。

此查询将选择的实际行数为 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。在此示例中,添加ORDER BY unique1将使用相同的计划,因为索引已经隐式提供了请求的排序。

规划器可以通过多种方式实现ORDER BY子句。上面的示例表明,此类排序子句可以隐式实现。规划器还可以添加一个显式的sort步骤

EXPLAIN SELECT * FROM tenk1 ORDER BY unique1;
                            QUERY PLAN
-------------------------------------------------------------------
 Sort  (cost=1109.39..1134.39 rows=10000 width=244)
   Sort Key: unique1
   ->  Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

如果计划的一部分保证对所需排序键的前缀进行排序,则规划器可能会决定改用增量排序步骤

EXPLAIN SELECT * FROM tenk1 ORDER BY four, ten LIMIT 100;
                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------------
 Limit  (cost=521.06..538.05 rows=100 width=244)
   ->  Incremental Sort  (cost=521.06..2220.95 rows=10000 width=244)
         Sort Key: four, ten
         Presorted Key: four
         ->  Index Scan using index_tenk1_on_four on tenk1  (cost=0.29..1510.08 rows=10000 width=244)

与常规排序相比,增量排序允许在对整个结果集进行排序之前返回元组,这尤其可以对LIMIT查询进行优化。它还可以减少内存使用量和将排序溢出到磁盘的可能性,但代价是将结果集拆分为多个排序批次所带来的开销增加。

如果在 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 子句,因此只能应用于连接点,而不能应用于任一输入扫描。以下是一个示例

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,则首选顺序扫描和排序,因为该表中需要访问更多行。(顺序扫描和排序通常优于索引扫描以对许多行进行排序,因为索引扫描需要非顺序磁盘访问。)

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

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

这些计数对于应用于联接节点的筛选条件特别有价值。已删除的行行仅在至少一个扫描行或联接节点中的潜在联接对被筛选条件拒绝时出现。

与筛选条件类似的情况发生在有损索引扫描中。例如,考虑对包含特定点的多边形的此搜索

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.08..230.08 rows=0 width=0) (actual time=3.791..3.792 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.08..230.08 rows=102 width=10) (actual time=0.069..0.513 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         Heap Blocks: exact=90
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.05 rows=102 width=0) (actual time=0.036..0.037 rows=300 loops=1)
               Index Cond: (unique1 < 100)
 Planning Time: 0.113 ms
 Execution Time: 3.850 ms

ROLLBACK;

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

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

EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------------
 Update on parent  (cost=0.00..24.59 rows=0 width=0)
   Update on parent parent_1
   Update on child1 parent_2
   Update on child2 parent_3
   Update on child3 parent_4
   ->  Result  (cost=0.00..24.59 rows=4 width=14)
         ->  Append  (cost=0.00..24.54 rows=4 width=14)
               ->  Seq Scan on parent parent_1  (cost=0.00..0.00 rows=1 width=14)
                     Filter: (f1 = 101)
               ->  Index Scan using child1_pkey on child1 parent_2  (cost=0.15..8.17 rows=1 width=14)
                     Index Cond: (f1 = 101)
               ->  Index Scan using child2_pkey on child2 parent_3  (cost=0.15..8.17 rows=1 width=14)
                     Index Cond: (f1 = 101)
               ->  Index Scan using child3_pkey on child3 parent_4  (cost=0.15..8.17 rows=1 width=14)
                     Index Cond: (f1 = 101)

在此示例中,更新节点需要考虑三个子表以及最初提到的父表。因此,每个表都有四个输入扫描子计划。为了清楚起见,更新节点带注释,以显示将被更新的特定目标表,与相应的子计划顺序相同。

EXPLAIN ANALYZE 显示的 规划时间 是从已解析的查询生成查询计划并对其进行优化所需的时间。不包括解析或重写。

EXPLAIN ANALYZE 显示的 执行时间 包括执行器启动和关闭时间,以及运行任何触发的触发时间,但不包括解析、重写或规划时间。在 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 节点始终将其实际行计数报告为零。

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