支持的版本: 当前 (17) / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2

14.1. 使用 EXPLAIN #

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

本节中的示例来自回归测试数据库,在执行VACUUM ANALYZE之后,使用v17的开发源代码。如果您自己尝试这些示例,您应该能够获得类似的结果,但是您的估计成本和行数可能会略有不同,因为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..445.00 rows=10000 width=244)

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

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

  • 估计的总成本。 这是假设计划节点运行完成,即检索所有可用行的前提下说明的。实际上,节点的父节点可能会停止读取所有可用的行(请参见下面的LIMIT示例)。

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

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

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

重要的是要理解,上层节点的成本包括其所有子节点的成本。同样重要的是要意识到,成本仅反映了计划器关心的事情。特别是,成本不考虑将输出值转换为文本形式或将其传输到客户端所花费的时间,这可能是实际运行时间中的重要因素;但是计划器会忽略这些成本,因为它无法通过更改计划来更改它们。(我们相信,每个正确的计划都会输出相同的行集。)

rows值有点棘手,因为它不是计划节点处理或扫描的行数,而是节点发出的行数。由于节点上应用的任何WHERE子句条件的过滤,这通常少于扫描的行数。理想情况下,顶层的行估计值将近似于查询实际返回、更新或删除的行数。

回到我们的示例

EXPLAIN SELECT * FROM tenk1;

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

这些数字的推导非常简单。 如果你这样做

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

您会发现tenk1 有 345 个磁盘页面和 10000 行。估计的成本计算为(读取的磁盘页面 * seq_page_cost)+(扫描的行 * cpu_tuple_cost)。默认情况下,seq_page_cost 为 1.0,cpu_tuple_cost 为 0.01,因此估计成本为 (345 * 1.0) + (10000 * 0.01) = 445。

现在让我们修改查询以添加一个WHERE条件

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

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

请注意,EXPLAIN 输出显示 WHERE 子句被应用为附加到 Seq Scan 计划节点的 过滤器 条件。这意味着计划节点会检查它扫描的每一行的条件,并且仅输出通过条件的行。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.06..224.98 rows=100 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 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..225.20 rows=1 width=244)
   Recheck Cond: (unique1 < 100)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 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)

如果计划的某一部分保证了所需排序键前缀的排序,则规划器可能会决定使用 Incremental Sort 步骤。

EXPLAIN SELECT * FROM tenk1 ORDER BY hundred, ten LIMIT 100;

                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------
 Limit  (cost=19.35..39.49 rows=100 width=244)
   ->  Incremental Sort  (cost=19.35..2033.39 rows=10000 width=244)
         Sort Key: hundred, ten
         Presorted Key: hundred
         ->  Index Scan using tenk1_hundred on tenk1  (cost=0.29..1574.20 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.07..60.11 rows=10 width=244)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 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.28 rows=2 width=244)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.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.50 rows=10 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 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.90 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.90),再加上一些用于连接处理的 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.36 rows=33 width=488)
   Join Filter: (t1.hundred < t2.hundred)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 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=226.23..709.73 rows=100 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
   ->  Hash  (cost=224.98..224.98 rows=100 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 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=0.56..233.49 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..643.28 rows=100 width=244)
         Filter: (unique1 < 100)
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..166.28 rows=1000 width=244)

合并连接要求其输入数据按连接键排序。在此示例中,每个输入都通过使用索引扫描以正确的顺序访问行来进行排序;但是也可以使用顺序扫描和排序。(由于索引扫描需要非顺序磁盘访问,因此对于排序许多行,顺序扫描和排序经常胜过索引扫描。)

查看变体计划的一种方法是,使用 第 19.7.1 节中描述的启用/禁用标志,强制规划器忽略其认为最便宜的策略。(这是一个粗略的工具,但很有用。另请参见第 14.3 节。)例如,如果我们不确信对于前面的示例,合并连接是最佳的连接类型,我们可以尝试。

SET enable_mergejoin = off;

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

                                        QUERY PLAN
-------------------------------------------------------------------​-----------------------
 Hash Join  (cost=226.23..344.08 rows=10 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on onek t2  (cost=0.00..114.00 rows=1000 width=244)
   ->  Hash  (cost=224.98..224.98 rows=100 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                     Index Cond: (unique1 < 100)

这表明,规划器认为对于这种情况,哈希连接的成本将比合并连接高出近 50%。当然,下一个问题是它是否正确。我们可以使用 EXPLAIN ANALYZE 进行调查,如下文所述

某些查询计划涉及子计划,这些子计划来自原始查询中的子 SELECT。有时可以将此类查询转换为普通连接计划,但是当它们不能时,我们会得到如下计划。

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t
WHERE t.ten < ALL (SELECT o.ten FROM onek o WHERE o.four = t.four);

                               QUERY PLAN
-------------------------------------------------------------------​------
 Seq Scan on public.tenk1 t  (cost=0.00..586095.00 rows=5000 width=4)
   Output: t.unique1
   Filter: (ALL (t.ten < (SubPlan 1).col1))
   SubPlan 1
     ->  Seq Scan on public.onek o  (cost=0.00..116.50 rows=250 width=4)
           Output: o.ten
           Filter: (o.four = t.four)

这个相当人为的例子用来解释几个要点:外部计划级别的值可以传递到子计划中(此处,t.four 被传递下来),子选择的结果可用于外部计划。这些结果值由 EXPLAIN 显示,并带有类似 (subplan_name).colN 的符号,该符号引用子 SELECT 的第N个输出列。

在上面的示例中,ALL 运算符为外部查询的每一行再次运行子计划(这说明了估计成本高的原因)。某些查询可以使用哈希子计划来避免这种情况。

EXPLAIN SELECT *
FROM tenk1 t
WHERE t.unique1 NOT IN (SELECT o.unique1 FROM onek o);

                                         QUERY PLAN
-------------------------------------------------------------------​-------------------------
 Seq Scan on tenk1 t  (cost=61.77..531.77 rows=5000 width=244)
   Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1)))
   SubPlan 1
     ->  Index Only Scan using onek_unique1 on onek o  (cost=0.28..59.27 rows=1000 width=4)
(4 rows)

在这里,子计划仅运行一次,其输出将加载到内存中的哈希表中,然后由外部 ANY 运算符探测。这要求子 SELECT 不引用外部查询的任何变量,并且 ANY 的比较运算符适合哈希。

如果除了不引用外部查询的任何变量之外,子 SELECT 不能返回多于一行,则它可以改为作为 initplan 实现。

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);

                             QUERY PLAN
------------------------------------------------------------​--------
 Seq Scan on public.tenk1 t1  (cost=0.02..470.02 rows=1000 width=4)
   Output: t1.unique1
   Filter: (t1.ten = (InitPlan 1).col1)
   InitPlan 1
     ->  Result  (cost=0.00..0.02 rows=1 width=4)
           Output: ((random() * '10'::double precision))::integer

initplan 对于外部计划的每次执行仅运行一次,其结果会保存起来,以便在外部计划的后续行中重复使用。因此,在此示例中,random() 仅评估一次,并且 t1.ten 的所有值都与同一随机选择的整数进行比较。这与没有子 SELECT 构造的情况截然不同。

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.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
         Recheck Cond: (unique1 < 10)
         Heap Blocks: exact=10
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
         Index Cond: (unique2 = t1.unique2)
 Planning Time: 0.485 ms
 Execution Time: 0.073 ms

请注意,实际时间值以实际时间的毫秒为单位,而 cost 估计值以任意单位表示;因此它们不太可能匹配。通常最需要注意的事情是估计的行计数是否与现实情况相当接近。在此示例中,估计值全部准确,但这在实践中非常罕见。

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

在某些情况下,EXPLAIN ANALYZE 显示的执行统计信息超出了计划节点执行时间和行计数。例如,Sort 和 Hash 节点提供了额外的信息。

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=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100 loops=1)
   Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 74kB
   ->  Hash Join  (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 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.026..1.790 rows=10000 loops=1)
         ->  Hash  (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 35kB
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100 loops=1)
                     Recheck Cond: (unique1 < 100)
                     Heap Blocks: exact=90
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                           Index Cond: (unique1 < 100)
 Planning Time: 0.187 ms
 Execution Time: 3.036 ms

Sort 节点显示所用的排序方法(特别是排序是在内存中还是在磁盘上)以及所需的内存或磁盘空间量。Hash 节点显示哈希桶和批次的数量以及用于哈希表的峰值内存量。(如果批次数超过一个,则还会涉及磁盘空间使用,但未显示。)

另一种额外信息是过滤器条件删除的行数。

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

                                               QUERY PLAN
-------------------------------------------------------------------​--------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
 Planning Time: 0.102 ms
 Execution Time: 2.145 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.09 rows=1 width=85) (actual time=0.023..0.023 rows=0 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 7
 Planning Time: 0.039 ms
 Execution Time: 0.033 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=85) (actual time=0.074..0.074 rows=0 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
 Planning Time: 0.039 ms
 Execution Time: 0.098 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.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10 loops=1)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Heap Blocks: exact=10
   Buffers: shared hit=14 read=3
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
         Buffers: shared hit=4 read=3
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 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.070..0.070 rows=999 loops=1)
               Index Cond: (unique2 > 9000)
               Buffers: shared hit=2 read=3
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.162 ms
 Execution Time: 0.143 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.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         Heap Blocks: exact=90
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
               Index Cond: (unique1 < 100)
 Planning Time: 0.151 ms
 Execution Time: 1.856 ms

ROLLBACK;

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

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

EXPLAIN UPDATE gtest_parent SET f1 = CURRENT_DATE WHERE f2 = 101;

                                       QUERY PLAN
-------------------------------------------------------------------​---------------------
 Update on gtest_parent  (cost=0.00..3.06 rows=0 width=0)
   Update on gtest_child gtest_parent_1
   Update on gtest_child2 gtest_parent_2
   Update on gtest_child3 gtest_parent_3
   ->  Append  (cost=0.00..3.06 rows=3 width=14)
         ->  Seq Scan on gtest_child gtest_parent_1  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child2 gtest_parent_2  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child3 gtest_parent_3  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)

在此示例中,Update 节点需要考虑三个子表,而不是最初提到的分区表(因为它永远不会存储任何数据)。因此,每个表都有三个输入扫描子计划。为了清晰起见,Update 节点被注释为显示将要更新的特定目标表,其顺序与相应的子计划相同。

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

EXPLAIN ANALYZE 显示的 Execution time 包括执行器启动和关闭时间,以及运行任何触发器的时间,但不包括解析、重写或规划时间。如果存在,则执行 BEFORE 触发器所花费的时间包含在相关的 Insert、Update 或 Delete 节点的时间中;但是执行 AFTER 触发器所花费的时间不计算在内,因为 AFTER 触发器是在整个计划完成后触发的。每个触发器(BEFOREAFTER)中花费的总时间也会单独显示。请注意,延迟约束触发器将不会执行,直到事务结束,因此 EXPLAIN ANALYZE 根本不考虑它们。

顶层节点显示的时间不包括将查询的输出数据转换为可显示形式或将其发送到客户端所需的任何时间。虽然 EXPLAIN ANALYZE 永远不会将数据发送到客户端,但可以通过指定 SERIALIZE 选项来告知其将查询的输出数据转换为可显示的形式并测量所需的时间。该时间将单独显示,并且也包含在总 Execution time 中。

14.1.3. 注意事项 #

EXPLAIN ANALYZE 测量的运行时间与同一查询的正常执行之间存在两种显着的偏差方式。首先,由于没有输出行传递到客户端,因此不包括网络传输成本。除非指定 SERIALIZE,否则也不包括 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.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
 Planning Time: 0.077 ms
 Execution Time: 0.086 ms

Index Scan 节点的估计成本和行数显示为好像它已运行完成。但实际上,Limit 节点在获取两个行后停止请求行,因此实际行数仅为 2,并且运行时间小于成本估算所暗示的时间。这不是估算错误,只是估算值和真实值显示方式的差异。

合并连接也具有可能会混淆粗心大意者的测量伪像。如果合并连接已用完另一个输入并且一个输入中的下一个键值大于另一个输入的最后一个键值,则合并连接将停止读取一个输入;在这种情况下,可能不会有更多的匹配项,因此无需扫描第一个输入的其余部分。这会导致不读取一个子项的所有内容,其结果类似于前面提到的 LIMIT。此外,如果外部(第一个)子项包含具有重复键值的行,则内部(第二个)子项将被备份并重新扫描以查找与该键值匹配的部分行。EXPLAIN ANALYZE 将这些重复发射的相同内部行视为真实的附加行。当外部重复项很多时,内部子计划节点报告的实际行数可能明显大于内部关系中实际的行数。

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

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

提交更正

如果您在文档中发现任何不正确的内容、与您特定功能的体验不符或需要进一步澄清的内容,请使用此表格来报告文档问题。