2025年9月25日: PostgreSQL 18 发布!
支持的版本:当前 (18) / 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 之后,使用 v18 的开发源代码。如果您尝试自行执行示例,应该会得到类似的结果,但您估计的成本和行数可能会略有不同,因为 ANALYZE 的统计信息是随机抽样而不是精确的,并且成本本质上在某种程度上是平台相关的。

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

14.1.1. EXPLAIN 基础知识 #

查询计划的结构是计划节点的树。树的底层节点是扫描节点:它们从表中返回原始行。对于不同的表访问方法,存在不同的扫描节点类型:顺序扫描、索引扫描和位图索引扫描。还有非表行源,例如 FROM 中的 VALUES 子句和集合返回函数,它们有自己的扫描节点类型。如果查询需要对原始行进行连接、聚合、排序或其他操作,那么在扫描节点之上将会有额外的节点来执行这些操作。同样,通常有不止一种执行这些操作的方法,因此这里也可能出现不同的节点类型。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 子句作为附加到顺序扫描计划节点的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)

在此计划中,我们有一个嵌套循环连接节点,带有两个表扫描作为输入或子节点。节点摘要行的缩进反映了计划树结构。连接的第一个(或outer)子节点是类似我们之前看到的位图扫描。其成本和行数与我们从 SELECT ... WHERE unique1 < 10 得到的相同,因为我们在该节点应用了 WHERE 子句 unique1 < 10t1.unique2 = t2.unique2 子句尚未相关,因此它不影响外部扫描的行数。嵌套循环连接节点将为其第二个(或inner)子节点执行一次,用于从外部子节点获取的每一行。来自当前外部行的列值可以插入到内部扫描中;在这里,外部行的 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 节点在读取数据时将其存储在内存中,然后在每次后续传递中从内存返回数据。

在处理外部连接时,您可能会看到带有 Join Filter 和普通 Filter 条件的连接计划节点。Join Filter 条件来自外部连接的 ON 子句,因此一个失败 Join Filter 条件的行仍然可以作为空扩展行发出。但普通 Filter 条件在外部连接规则之后应用,因此会无条件地移除行。在内部连接中,这些类型的过滤器之间没有语义差异。

如果我们稍微更改查询的选择性,我们可能会得到一个非常不同的连接计划

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 节中描述的 enable/disable 标志。(这是一个粗略的工具,但很有用。另请参阅 第 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 进行调查,如下文所述。

在使用 enable/disable 标志禁用计划节点类型时,许多标志仅阻止使用相应的计划节点,而不会完全剥夺规划器使用计划节点类型的能力。这是故意的,以便规划器仍能为给定的查询形成一个计划。当生成的计划包含一个禁用的节点时,EXPLAIN 输出将指示此事实。

SET enable_seqscan = off;
EXPLAIN SELECT * FROM unit;

                       QUERY PLAN
---------------------------------------------------------
 Seq Scan on unit  (cost=0.00..21.30 rows=1130 width=44)
   Disabled: true

由于 unit 表没有索引,因此没有其他方法可以读取表数据,因此顺序扫描是查询规划器可用的唯一选项。

某些查询计划涉及子计划,这些子计划来自原始查询中的子 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 不引用外部查询的任何变量,并且子 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.00 loops=1)
   Buffers: shared hit=36 read=6
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10.00 loops=1)
         Recheck Cond: (unique1 < 10)
         Heap Blocks: exact=10
         Buffers: shared hit=3 read=5 written=4
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10.00 loops=1)
               Index Cond: (unique1 < 10)
               Index Searches: 1
               Buffers: shared hit=2
   ->  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.00 loops=10)
         Index Cond: (unique2 = t1.unique2)
         Index Searches: 10
         Buffers: shared hit=24 read=6
 Planning:
   Buffers: shared hit=15 dirtied=9
 Planning Time: 0.485 ms
 Execution Time: 0.073 ms

请注意,“actual time”值以毫秒为单位的真实时间表示,而 cost 估计以任意单位表示;因此不太可能匹配。通常最重要的是查看估计的行数是否与实际情况 reasonably close。在此示例中,估计值都非常准确,但这在实践中相当不寻常。

在某些查询计划中,子计划节点可能被执行多次。例如,在上面的嵌套循环计划中,内部索引扫描将为每个外部行执行一次。在这种情况下,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.00 loops=1)
   Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 74kB
   Buffers: shared hit=440
   ->  Hash Join  (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100.00 loops=1)
         Hash Cond: (t2.unique2 = t1.unique2)
         Buffers: shared hit=437
         ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000.00 loops=1)
               Buffers: shared hit=345
         ->  Hash  (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100.00 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 35kB
               Buffers: shared hit=92
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100.00 loops=1)
                     Recheck Cond: (unique1 < 100)
                     Heap Blocks: exact=90
                     Buffers: shared hit=92
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100.00 loops=1)
                           Index Cond: (unique1 < 100)
                           Index Searches: 1
                           Buffers: shared hit=2
 Planning:
   Buffers: shared hit=12
 Planning Time: 0.187 ms
 Execution Time: 3.036 ms

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

Index Scan 节点(以及 Bitmap Index Scan 和 Index-Only Scan 节点)显示“Index Searches”行,该行报告 所有 节点执行/ loops 的总搜索次数

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 500, 700, 999);
                                                            QUERY PLAN
-------------------------------------------------------------------​---------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=9.45..73.44 rows=40 width=244) (actual time=0.012..0.028 rows=40.00 loops=1)
   Recheck Cond: (thousand = ANY ('{1,500,700,999}'::integer[]))
   Heap Blocks: exact=39
   Buffers: shared hit=47
   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=0.00..9.44 rows=40 width=0) (actual time=0.009..0.009 rows=40.00 loops=1)
         Index Cond: (thousand = ANY ('{1,500,700,999}'::integer[]))
         Index Searches: 4
         Buffers: shared hit=8
 Planning Time: 0.029 ms
 Execution Time: 0.034 ms

这里我们看到一个 Bitmap Index Scan 节点,它需要 4 次单独的索引搜索。对于谓词的 IN 构造的每个 integer 值,扫描需要从 tenk1_thous_tenthous 索引的根页面开始搜索索引一次。但是,索引搜索的数量通常与查询谓词没有这种简单的对应关系

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE thousand IN (1, 2, 3, 4);
                                                            QUERY PLAN
-------------------------------------------------------------------​---------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=9.45..73.44 rows=40 width=244) (actual time=0.009..0.019 rows=40.00 loops=1)
   Recheck Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
   Heap Blocks: exact=38
   Buffers: shared hit=40
   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=0.00..9.44 rows=40 width=0) (actual time=0.005..0.005 rows=40.00 loops=1)
         Index Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
         Index Searches: 1
         Buffers: shared hit=2
 Planning Time: 0.029 ms
 Execution Time: 0.026 ms

IN 查询的变体只执行了 1 次索引搜索。它在遍历索引上花费的时间(与原始查询相比)更少,因为其 IN 构造使用的值匹配存储在相邻索引元组上的值,在同一个 tenk1_thous_tenthous 索引叶子页面上。

Index Searches”行对于应用跳过扫描优化以更有效地遍历索引的 B-tree 索引扫描也很有用

EXPLAIN ANALYZE SELECT four, unique1 FROM tenk1 WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
                                                              QUERY PLAN
-------------------------------------------------------------------​---------------------------------------------------------------
 Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
   Index Cond: ((four >= 1) AND (four <= 3) AND (unique1 = 42))
   Heap Fetches: 0
   Index Searches: 3
   Buffers: shared hit=7
 Planning Time: 0.029 ms
 Execution Time: 0.012 ms

这里我们看到一个 Index-Only Scan 节点使用了 tenk1_four_unique1_idx,这是一个多列索引,位于 tenk1 表的 fourunique1 列上。扫描执行 3 次搜索,每次读取一个索引叶子页面:“four = 1 AND unique1 = 42”、“four = 2 AND unique1 = 42”和“four = 3 AND unique1 = 42”。此索引通常是跳过扫描的一个好目标,因为正如第 11.3 节中所述,其前导列(four 列)仅包含 4 个不同的值,而其第二/最后一列(unique1 列)包含许多不同的值。

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

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.00 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
   Buffers: shared hit=345
 Planning Time: 0.102 ms
 Execution Time: 2.145 ms

这些计数对于应用于连接节点的过滤器条件尤其有价值。“Rows Removed”行仅在至少一行扫描或连接节点情况下的潜在连接对被过滤器条件拒绝时出现。

与过滤器条件类似的情况发生在有损索引扫描中。例如,考虑以下搜索包含特定点的多边形

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.00 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 7
   Buffers: shared hit=1
 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.00 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
   Index Searches: 1
   Buffers: shared hit=1
 Planning Time: 0.039 ms
 Execution Time: 0.098 ms

在这里,我们可以看到索引返回了一个候选行,然后该行被索引条件的重查拒绝。发生这种情况是因为 GiST 索引对于多边形包含测试是有损的:它实际返回与目标重叠的多边形所在的行,然后我们必须在这些行上执行精确的包含测试。

EXPLAIN 有一个 BUFFERS 选项,它提供了有关查询的计划和执行期间执行的 I/O 操作的额外详细信息。显示的缓冲区编号显示了给定节点及其所有子节点使用的非重复缓冲区(命中、读取、脏、写入)的计数。ANALYZE 选项会自动启用 BUFFERS 选项。如果不需要,可以显式禁用 BUFFERS

EXPLAIN (ANALYZE, BUFFERS OFF) 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.00 loops=1)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Heap Blocks: exact=10
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0.00 loops=1)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100.00 loops=1)
               Index Cond: (unique1 < 100)
               Index Searches: 1
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999.00 loops=1)
               Index Cond: (unique2 > 9000)
               Index Searches: 1
 Planning Time: 0.162 ms
 Execution Time: 0.143 ms

请记住,由于 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.00 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100.00 loops=1)
         Recheck Cond: (unique1 < 100)
         Heap Blocks: exact=90
         Buffers: shared hit=4 read=2
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100.00 loops=1)
               Index Cond: (unique1 < 100)
               Index Searches: 1
               Buffers: shared read=2
 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.00 loops=1)
   Buffers: shared hit=16
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2.00 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
         Index Searches: 1
         Buffers: shared hit=16
 Planning Time: 0.077 ms
 Execution Time: 0.086 ms

索引扫描节点的估计成本和行数显示为假定已完全执行。但实际上,Limit 节点在获得两行后就停止请求行,因此实际行数仅为 2,运行时间也小于成本估计值。这不是估计错误,仅仅是估计和真实值显示方式上的差异。

合并连接也有可能使不熟悉的人感到困惑的测量伪影。如果合并连接耗尽了另一个输入,并且第一个输入中的下一个键值大于第二个输入的最后一个键值,那么合并连接将停止读取一个输入;在这种情况下,不可能有更多匹配,因此无需扫描第一个输入的其余部分。这会导致未读取所有子节点,结果与 LIMIT 中提到的类似。此外,如果外部(第一个)子节点包含重复键值的行,则内部(第二个)子节点将被备份并重新扫描其与该键值匹配的行部分。EXPLAIN ANALYZE 将这些内部行的重复发出计算为实际附加行。当存在许多外部重复项时,报告的内部子计划节点的实际行数可能远大于内部关系中实际存在的行数。

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

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

提交更正

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