受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

Chapter 14. 性能提示

查询性能可能受许多因素影响。其中一些因素可以由用户控制,另一些则属于系统底层设计的基本特性。本章提供一些帮助理解和调优PostgreSQL性能的提示。

14.1. 使用EXPLAIN #

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..445.00 rows=10000 width=244)

由于这个查询没有WHERE子句,它必须扫描表中的所有行,因此规划器选择了一个简单的顺序扫描计划。圆括号中的数字从左到右依次表示:

  • 估计启动开销。这是输出阶段开始前需要消耗的时间,例如排序节点执行排序所需的时间。

  • 估计总开销。这里假定该计划节点会运行到结束,也就是取回所有可用的行。实际中某个节点的父节点可能会在尚未读完所有可用行之前提前停止(见后文的LIMIT示例)。

  • 该计划节点输出行数的估计值。同样,也是假定该节点会运行到结束。

  • 预计该计划节点输出行的平均宽度,以字节计。

这些开销使用由规划器代价参数决定的任意单位来衡量(见Section 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,因此不必检索全部行,规划器也就改变了选择。注意,Index Scan 节点的总代价和行计数显示得像是它会运行到结束一样;但 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 < 10正是在该节点上应用的。t1.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 节点会在读取数据时将其保存在内存中,并在之后的每次遍历中从内存返回这些数据。

在处理外连接时,可能会看到连接计划节点同时带有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 节点据此构造哈希表;随后该哈希表被返回给 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)

归并连接要求输入数据按连接键排序。在这个例子中,两个输入都通过索引扫描按正确顺序访问行而完成排序;不过也可以采用顺序扫描再排序的方式。(对于需要排序很多行的情况,顺序扫描加排序往往会胜过索引扫描,因为索引扫描需要非顺序磁盘访问。)

观察备选计划的一种方法,是利用Section 19.7.1中描述的启用/禁用标志,强迫规划器忽略它认为最便宜的策略。(这是个粗糙但有用的工具。另见Section 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来研究。

使用启用/禁用标志来关闭某类计划节点时,要注意许多标志只是抑制使用对应的计划节点,而不是彻底禁止规划器使用它。这是刻意设计的,这样规划器仍然能够为给定查询形成计划。如果最终计划中仍包含被禁用的节点,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表没有索引,因此没有其他方式可以读取其中的数据,顺序扫描就是查询规划器唯一可选的方案。

有些查询计划会涉及子计划(subplan),它们来自原始查询中的子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.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估计值则以任意单位表示,因此二者通常不会直接对应。通常最值得关注的是估计行数是否与实际情况足够接近。在这个示例中,估计值完全命中,但在实践中这其实相当少见。

在某些查询计划中,一个子计划节点可能会执行多次。例如,上面那个嵌套循环计划中的内侧索引扫描会对外侧的每一行执行一次。在这种情况下,loops值报告的是该节点的总执行次数,而 actual time 和 rows 显示的是每次执行的平均值。这样做是为了让这些数字更容易与代价估计的展示方式相比较。将它们乘以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 节点则会显示哈希桶和批次的数量,以及哈希表使用内存的峰值。(如果批次数超过 1,还会涉及磁盘空间使用,但这里不会显示出来。)

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这一行对于应用了跳跃扫描(skip scan)优化、以更高效方式遍历索引的 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

这里看到一个使用tenk1_four_unique1_idx的 Index-Only Scan 节点,该索引是tenk1表上由fourunique1两列组成的多列索引。该扫描执行了 3 次搜索,每次都只读取一个索引叶子页面:four = 1 AND unique1 = 42four = 2 AND unique1 = 42以及four = 3 AND unique1 = 42。正如Section 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 计划节点完成。该节点下面的计划节点负责定位旧行和/或计算新数据。因此,上面看到的是前文已经介绍过的同类位图表扫描,它的输出被送入一个负责存储更新后行的 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触发器是在整个计划完成之后才触发的。每个触发器,无论是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.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

Index Scan 节点的估计代价和行计数显示得像是它会运行到结束一样。但实际上,Limit 节点在拿到两行之后就停止请求更多行,因此实际行计数只有 2,运行时间也低于代价估计所暗示的水平。这并不是估计错误,只是估计值与真实值的显示方式不同而已。

归并连接也会产生一些容易误导人的计量现象。如果归并连接已经耗尽其中一个输入,而另一个输入中的下一个键值又大于前一个输入的最后一个键值,那么它就会停止继续读取前者;在这种情况下,不可能再有更多匹配,因此也就不需要扫描另一个输入的剩余部分。这会导致某个子节点没有被完整读取,其结果与前面提到的LIMIT情况类似。此外,如果外侧(第一个)子节点包含带有重复键值的行,内侧(第二个)子节点会被回退并重新扫描,以查找能够匹配该键值的行。EXPLAIN ANALYZE会把这些对同一内侧行的重复输出,统计得像是真实的额外行一样。当外侧存在大量重复值时,内侧子计划节点报告出的实际行计数,可能会明显大于内侧关系中真实存在的行数。

由于实现上的限制,BitmapAnd 和 BitmapOr 节点总是报告其实际行计数为零。

通常,EXPLAIN会显示规划器生成的每一个计划节点。不过在某些情况下,执行器能够根据那些在规划阶段尚不可用的参数值判断某些节点根本不需要执行,因为它们不可能产生任何行。(目前,这种情况只会发生在扫描分区表的 Append 或 MergeAppend 节点的子节点上。)当出现这种情况时,这些计划节点会从EXPLAIN输出中省略,转而显示一条Subplans Removed: N标注。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。