2024 年 9 月 26 日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持的版本: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 的默认 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..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 计划节点的 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.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)

这里,计划器决定使用一个两步计划:子计划节点访问索引以查找匹配索引条件的行的位置,然后上层计划节点从表本身中实际获取这些行。单独获取行比顺序读取行要昂贵得多,但由于不必访问表的全部页,因此仍然比顺序扫描便宜。(使用两个计划级别的原因是,上层计划节点在读取行之前将索引识别的行位置排序为物理顺序,以最大程度地减少单独获取的成本。在节点名称中提到的 bitmap 是执行排序的机制。)

现在让我们在 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 的位图扫描是 Hash 节点的输入,它构造哈希表。然后将其返回给 Hash Join 节点,该节点读取来自其外部子计划的行,并搜索哈希表中的每个行。

另一种可能的连接类型是合并连接,这里进行了说明

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 无法返回多于一行,它可以改为实现为 初始化计划

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

初始化计划在外部计划的每次执行中只运行一次,并且它的成果将保存以供外部计划的后续行重用。因此,在这个例子中,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 值以获得实际在节点中花费的总时间。在上面的例子中,我们总共花费了 0.030 毫秒在 tenk2 上执行索引扫描。

在某些情况下,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 节点显示哈希桶和批次的数量,以及用于哈希表的峰值内存使用量。(如果批次数超过 1,也会涉及磁盘空间使用,但不会显示。)

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

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 节点的 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 包括执行程序启动和关闭时间,以及运行任何触发的触发器的时间,但不包括解析、重写或计划时间。在相关 Insert、Update 或 Delete 节点的时间内包含执行 BEFORE 触发器(如果有)所花费的时间;但执行 AFTER 触发器所花费的时间不计入其中,因为 AFTER 触发器在整个计划完成之后触发。在每个触发器(无论是 BEFORE 还是 AFTER)中花费的总时间也单独显示。请注意,延迟约束触发器将在事务结束之前执行,因此 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

索引扫描节点的估计成本和行数显示为它被完全运行时的样子。但实际上,Limit 节点在获得两行后停止请求行,因此实际行数仅为 2,运行时间小于成本估计所建议的时间。这不是估计错误,只是估计值和真实值显示方式的差异。

合并连接也有可能导致混淆无知者的测量伪像。如果耗尽了另一个输入,并且一个输入中的下一个键值大于另一个输入的最后一个键值,则合并连接将停止读取一个输入;在这种情况下,不会有更多匹配项,因此无需扫描第一个输入的剩余部分。这会导致没有读取一个子节点的所有内容,从而产生与 LIMIT 提到的结果类似的结果。此外,如果外部(第一个)子节点包含具有重复键值的行,则内部(第二个)子节点将被备份并重新扫描以匹配该键值的其部分行。 EXPLAIN ANALYZE 将这些相同内部行的重复发出视为实际的额外行。当存在许多外部重复项时,内部子节点计划节点报告的实际行数可能远远大于实际上在内部关系中的行数。

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

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

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步说明,请使用 此表格 报告文档问题。