查询性能可能受许多因素影响。其中一些因素可以由用户控制,另一些则属于系统底层设计的基本特性。本章提供一些帮助理解和调优PostgreSQL性能的提示。
EXPLAIN #PostgreSQL会为收到的每个查询制定一个查询计划。选择与查询结构和数据特性相匹配的正确计划,对获得良好的性能至关重要,因此系统内置了一个复杂的规划器来尽量选出好的计划。可以使用EXPLAIN命令查看规划器为任意查询生成的查询计划。读懂计划是一门需要经验积累的技能,本节将尝试介绍其中的基础知识。
本节中的示例取自执行过VACUUM ANALYZE的回归测试数据库,使用的是 9.3 开发版本源码。如果自行尝试这些示例,通常应当能得到相近的结果,但估计代价和行计数可能略有差异,因为ANALYZE生成的统计信息来自随机采样而非精确计数,而且代价本身在某种程度上也依赖于平台。
这些示例使用EXPLAIN默认的“text”输出格式,它紧凑且便于人工阅读。如果希望将EXPLAIN的输出交给程序做进一步分析,则应改用机器可读的输出格式(XML、JSON 或 YAML)。
EXPLAIN基础 #查询计划的结构是一棵由计划节点组成的树。树的最底层是扫描节点,它们从表中返回原始行。不同的表访问方法对应不同类型的扫描节点,例如顺序扫描、索引扫描和位图索引扫描。也有一些并非表的行来源,例如VALUES子句和FROM中的返回集合函数,它们也各自有对应的扫描节点类型。如果查询需要对原始行执行连接、聚合、排序或其他操作,那么扫描节点之上还会出现额外的节点来完成这些操作。同样,这些操作通常不止一种实现方式,因此这里也会出现不同的节点类型。EXPLAIN会为计划树中的每个节点输出一行,显示基本节点类型以及规划器对该计划节点执行代价的估计值。还可能出现相对节点摘要行缩进的附加行,用来显示该节点的更多属性。第一行,也就是最顶层节点的摘要行,给出了整个计划的估计总执行代价;规划器力图最小化的正是这个数字。
下面是一个简单的示例,仅用于展示输出的样子:
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
由于这个查询没有WHERE子句,它必须扫描表中的所有行,因此规划器选择了一个简单的顺序扫描计划。圆括号中的数字从左到右依次表示:
估计启动开销。这是输出阶段开始前需要消耗的时间,例如排序节点执行排序所需的时间。
估计总开销。这里假定该计划节点会运行到结束,也就是取回所有可用的行。实际中某个节点的父节点可能会在尚未读完所有可用行之前提前停止(见后文的LIMIT示例)。
该计划节点输出行数的估计值。同样,也是假定该节点会运行到结束。
预计该计划节点输出行的平均宽度,以字节计。
这些开销使用由规划器代价参数决定的任意单位来衡量(见Section 19.7.2)。传统上通常以磁盘页读取作为代价单位;也就是说,惯例上将seq_page_cost设为1.0,其他代价参数都相对它来设定。本节中的示例都使用默认代价参数。
需要理解的一点是,上层节点的开销包含了其所有子节点的开销。还要注意,这个开销只反映规划器关心的内容。特别是,它没有考虑将输出值转换为文本形式或传输给客户端所消耗的时间,而这些在实际耗时中可能是重要因素;但规划器会忽略这些代价,因为它无法通过改变计划来影响它们。(我们相信每个正确的计划都会输出相同的行集。)
rows值有些容易误解,因为它不是计划节点处理或扫描过的行数,而是该节点输出的行数。由于在该节点上应用的WHERE条件会过滤掉一部分扫描到的行,这个数字通常小于扫描行数。理想情况下,顶层的行数估计应当接近查询实际返回、更新或删除的行数。
回到我们的示例:
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
这些数字的来源非常直接。如果执行:
SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
可以看到tenk1有 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..483.00 rows=7001 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.07..229.20 rows=101 width=244)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0)
Index Cond: (unique1 < 100)
这里规划器决定使用两步计划:子计划节点访问索引,找出匹配索引条件的行的位置,然后上层计划节点再从表本身取出这些行。分别取出各行的代价远高于顺序读取,但由于不必访问表的所有页,这仍然比顺序扫描便宜。(使用两层计划的原因是,上层计划节点在读取行之前,会先把索引确定的行位置按物理顺序排列,以尽量降低逐行读取的代价。节点名称中的“位图”就是执行这种排序的机制。)
现在向WHERE子句添加另一个条件:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';
QUERY PLAN
------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=5.04..229.43 rows=1 width=244)
Recheck Cond: (unique1 < 100)
Filter: (stringu1 = 'xxx'::name)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0)
Index Cond: (unique1 < 100)
新增的条件stringu1 = 'xxx'降低了估计输出行数,却没有降低代价,因为仍然必须访问同一组行。注意,stringu1子句不能用作索引条件,因为该索引只建立在unique1列上。它只能作为过滤条件,应用到通过索引取出的行上。因此,代价实际上略有增加,以反映这项额外检查。
在某些情况下,规划器更倾向于一个“简单”索引扫描计划:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;
QUERY PLAN
-----------------------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 (cost=0.29..8.30 rows=1 width=244)
Index Cond: (unique1 = 42)
在这种计划中,表行按索引顺序取出,因此读取代价更高,但行数很少,不值得为排序行位置付出额外代价。对于仅取出一行的查询,最常见到这种计划。它也经常用于带有以下条件的查询:ORDER BY与索引顺序匹配,因为这时无需额外排序步骤就能满足ORDER BY。
如果在WHERE引用的多个列上分别建有索引,规划器可能选择将这些索引做 AND 或 OR 组合:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;
QUERY PLAN
-------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=25.08..60.21 rows=10 width=244)
Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
-> BitmapAnd (cost=25.08..25.08 rows=10 width=0)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0)
Index Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0)
Index Cond: (unique2 > 9000)
但这需要访问两个索引,因此与只使用一个索引、把另一个条件作为过滤条件相比,并不一定更划算。如果调整条件中的范围,就会看到计划相应改变。
下面用一个示例说明LIMIT:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;
QUERY PLAN
-------------------------------------------------------------------------------------
Limit (cost=0.29..14.48 rows=2 width=244)
-> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..71.27 rows=10 width=244)
Index Cond: (unique2 > 9000)
Filter: (unique1 < 100)
这与上面的查询相同,只是加上了LIMIT,因此不必检索全部行,规划器也就改变了选择。注意,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.62 rows=10 width=488)
-> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.47 rows=10 width=244)
Recheck Cond: (unique1 < 10)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0)
Index Cond: (unique1 < 10)
-> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.91 rows=1 width=244)
Index Cond: (unique2 = t1.unique2)
在这个计划中,有一个嵌套循环连接节点,它的两个输入,也就是两个子节点,都是表扫描。节点摘要行的缩进反映了计划树结构。连接的第一个子节点,也就是“外侧”子节点,是一个与前面见过的位图扫描类似的节点。它的代价和行计数与SELECT ... WHERE unique1 < 10得到的结果相同,因为WHERE子句unique1 < 10正是在该节点上应用的。t1.unique2 = t2.unique2子句此时还无关,因此不会影响外侧扫描的行计数。嵌套循环连接节点会对从外侧子节点得到的每一行执行一次第二个,也就是“内侧”子节点。当前外侧行中的列值可以代入内侧扫描;这里外侧行的t1.unique2值可用,因此得到的计划和代价与前面看到的简单SELECT ... WHERE t2.unique2 = 情形类似。(由于预期在对constantt2反复执行索引扫描期间会发生缓存命中,估计代价实际上比前面看到的略低一些。)随后,循环节点的代价建立在外侧扫描代价之上,再加上每个外侧行都要执行一次内侧扫描的代价(这里是 10 * 7.91),以及少量连接处理的 CPU 时间。
本例中,连接的输出行数等于两个扫描行数的乘积,但并非所有情况都如此,因为可能还有额外的WHERE子句同时涉及两个表,因此只能在连接处应用,不能应用到任一输入扫描。下面是一个示例:
EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;
QUERY PLAN
---------------------------------------------------------------------------------------------
Nested Loop (cost=4.65..49.46 rows=33 width=488)
Join Filter: (t1.hundred < t2.hundred)
-> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.47 rows=10 width=244)
Recheck Cond: (unique1 < 10)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0)
Index Cond: (unique1 < 10)
-> Materialize (cost=0.29..8.51 rows=10 width=244)
-> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..8.46 rows=10 width=244)
Index Cond: (unique2 < 10)
条件t1.hundred < t2.hundred不能在tenk2_unique2索引中检验,因此应用在连接节点上。这会减少连接节点的估计输出行数,但不改变任一输入扫描。
注意,这里规划器通过在连接的内侧关系之上放置一个 Materialize 计划节点,选择将其“物化”。这意味着t2索引扫描只会执行一次,尽管嵌套循环连接节点需要读取那份数据十次,也就是外侧关系的每一行都要读取一次。Materialize 节点会在读取数据时将其保存在内存中,并在之后的每次遍历中从内存返回这些数据。
在处理外连接时,可能会看到连接计划节点同时带有“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=230.47..713.98 rows=101 width=488)
Hash Cond: (t2.unique2 = t1.unique2)
-> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244)
-> Hash (cost=229.20..229.20 rows=101 width=244)
-> Bitmap Heap Scan on tenk1 t1 (cost=5.07..229.20 rows=101 width=244)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0)
Index Cond: (unique1 < 100)
这里规划器选择了哈希连接:先把一个表的行放入内存中的哈希表,然后扫描另一个表,并对其中每一行到哈希表中查找匹配。再次注意缩进如何反映计划结构:tenk1上的位图扫描是 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=198.11..268.19 rows=10 width=488)
Merge Cond: (t1.unique2 = t2.unique2)
-> Index Scan using tenk1_unique2 on tenk1 t1 (cost=0.29..656.28 rows=101 width=244)
Filter: (unique1 < 100)
-> Sort (cost=197.83..200.33 rows=1000 width=244)
Sort Key: t2.unique2
-> Seq Scan on onek t2 (cost=0.00..148.00 rows=1000 width=244)
归并连接要求输入数据按连接键排序。在这个计划中,tenk1 的数据通过索引扫描按正确顺序访问行来完成排序,而 onek 则更适合顺序扫描后再排序,因为需要访问该表中的更多行。(对大量行进行排序时,顺序扫描加排序往往优于索引扫描,因为索引扫描需要非顺序的磁盘访问。)
查看其他候选计划的一种方法,是使用Section 19.7.1中介绍的启用/禁用标志,强制规划器忽略它认为代价最低的策略。(这是一种粗略但有用的工具。另请参见Section 14.3。)例如,如果不确信顺序扫描加排序是处理上例中onek表的最佳方法,可以尝试:
SET enable_sort = off;
EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;
QUERY PLAN
------------------------------------------------------------------------------------------
Merge Join (cost=0.56..292.65 rows=10 width=488)
Merge Cond: (t1.unique2 = t2.unique2)
-> Index Scan using tenk1_unique2 on tenk1 t1 (cost=0.29..656.28 rows=101 width=244)
Filter: (unique1 < 100)
-> Index Scan using onek_unique2 on onek t2 (cost=0.28..224.79 rows=1000 width=244)
这表明规划器认为,通过索引扫描来排序onek,代价比顺序扫描加排序大约高 12%。当然,接下来的问题是它的判断是否正确。可以使用EXPLAIN ANALYZE来调查,下文将详细说明。
EXPLAIN ANALYZE #可以使用EXPLAIN的ANALYZE选项检查规划器估计的准确性。启用此选项后,EXPLAIN会实际执行查询,然后显示各个计划节点内累计的实际行数和实际运行时间,同时也显示普通EXPLAIN提供的估计值。例如,可能得到如下结果:
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=4.65..118.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)
-> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.47 rows=10 width=244) (actual time=0.057..0.121 rows=10 loops=1)
Recheck Cond: (unique1 < 10)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.024..0.024 rows=10 loops=1)
Index Cond: (unique1 < 10)
-> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.91 rows=1 width=244) (actual time=0.021..0.022 rows=1 loops=10)
Index Cond: (unique2 = t1.unique2)
Planning time: 0.181 ms
Execution time: 0.501 ms
注意,“actual time”的值以实际时间的毫秒数表示,而cost估计值使用任意单位,因此两者不太可能相等。通常最重要的是检查估计行数是否足够接近实际行数。在本例中,所有估计都完全准确,但实际中很少如此。
在某些查询计划中,一个子计划节点可能会执行多次。例如,上面那个嵌套循环计划中的内侧索引扫描会对外侧的每一行执行一次。在这种情况下,loops值报告的是该节点的总执行次数,而 actual time 和 rows 显示的是每次执行的平均值。这样做是为了让这些数字更容易与代价估计的展示方式相比较。将它们乘以loops值,就能得到该节点实际消耗的总时间。在上面的示例中,执行tenk2上的索引扫描总共花费了 0.220 毫秒。
在某些情况下,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=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)
Sort Key: t1.fivethous
Sort Method: quicksort Memory: 77kB
-> Hash Join (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 rows=100 loops=1)
Hash Cond: (t2.unique2 = t1.unique2)
-> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.007..2.583 rows=10000 loops=1)
-> Hash (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 28kB
-> Bitmap Heap Scan on tenk1 t1 (cost=5.07..229.20 rows=101 width=244) (actual time=0.080..0.526 rows=100 loops=1)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.049..0.049 rows=100 loops=1)
Index Cond: (unique1 < 100)
Planning time: 0.194 ms
Execution time: 8.008 ms
Sort 节点显示所用的排序方法(尤其是排序在内存中还是磁盘上进行),以及所需的内存或磁盘空间。Hash 节点显示哈希桶数、批次数,以及哈希表的内存用量峰值。(如果批次数超过一,还会使用磁盘空间,但此处不显示。)
另一种附加信息是被过滤条件排除的行数:
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;
QUERY PLAN
---------------------------------------------------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)
Filter: (ten < 7)
Rows Removed by Filter: 3000
Planning time: 0.083 ms
Execution time: 5.905 ms
对于应用在连接节点上的过滤条件,这些计数尤其有价值。只有当至少一个扫描行(对于连接节点,则是一个潜在连接行对)被过滤条件排除时,才会出现“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.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1)
Filter: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Filter: 4
Planning time: 0.040 ms
Execution time: 0.083 ms
规划器认为(而且完全正确)这个示例表太小,不值得使用索引扫描,因此得到的是普通顺序扫描,其中所有行都被过滤条件排除。但如果强制使用索引扫描,就会看到:
SET enable_seqscan TO off;
EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Index Scan using gpolygonind on polygon_tbl (cost=0.13..8.15 rows=1 width=32) (actual time=0.062..0.062 rows=0 loops=1)
Index Cond: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Index Recheck: 1
Planning time: 0.034 ms
Execution time: 0.144 ms
这里可以看到,索引返回了一个候选行,但随后在重新检查索引条件时被排除。这是因为 GiST 索引对于多边形包含关系测试是“有损”的:它实际返回的是包含与目标重叠的多边形的行,然后必须对这些行进行精确的包含关系测试。
EXPLAIN有一个BUFFERS选项,可以与ANALYZE一起使用,以获取更多运行时统计信息:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=25.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1)
Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
Buffers: shared hit=15
-> BitmapAnd (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1)
Buffers: shared hit=7
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
Index Cond: (unique1 < 100)
Buffers: shared hit=2
-> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.227..0.227 rows=999 loops=1)
Index Cond: (unique2 > 9000)
Buffers: shared hit=5
Planning time: 0.088 ms
Execution time: 0.423 ms
由BUFFERS提供的数值有助于找出查询中 I/O 最密集的部分。
请记住,由于EXPLAIN ANALYZE会实际执行查询,任何副作用都会照常发生,即使查询原本输出的结果被丢弃,改为打印EXPLAIN数据。如果想分析会修改数据的查询而不改变表,可以在之后回滚命令,例如:
BEGIN;
EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Update on tenk1 (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1)
-> Bitmap Heap Scan on tenk1 (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
Index Cond: (unique1 < 100)
Planning time: 0.079 ms
Execution time: 14.727 ms
ROLLBACK;
如本例所示,当查询是INSERT、UPDATE或DELETE命令时,真正执行表修改的工作由顶层的 Insert、Update或 Delete 计划节点完成。该节点下面的计划节点负责定位旧行和/或计算新数据。因此,上面看到的是前文已经介绍过的同类位图表扫描,它的输出被送入一个负责存储更新后行的 Update 节点。值得注意的是,虽然数据修改节点可能占用相当可观的运行时间(这里它消耗了大部分时间),但规划器目前不会为这部分工作额外增加任何代价估计。这是因为这项工作对每个正确的查询计划都相同,因此不会影响规划决策。
当一个UPDATE或DELETE命令影响到继承层次结构时,输出可能如下所示:
EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
QUERY PLAN
-----------------------------------------------------------------------------------
Update on parent (cost=0.00..24.53 rows=4 width=14)
Update on parent
Update on child1
Update on child2
Update on child3
-> Seq Scan on parent (cost=0.00..0.00 rows=1 width=14)
Filter: (f1 = 101)
-> Index Scan using child1_f1_key on child1 (cost=0.15..8.17 rows=1 width=14)
Index Cond: (f1 = 101)
-> Index Scan using child2_f1_key on child2 (cost=0.15..8.17 rows=1 width=14)
Index Cond: (f1 = 101)
-> Index Scan using child3_f1_key on child3 (cost=0.15..8.17 rows=1 width=14)
Index Cond: (f1 = 101)
在本例中,Update 节点需要考虑三个子表,以及最初指定的父表。因此有四个输入扫描子计划,每个表一个。为清晰起见,Update 节点标注了将被更新的具体目标表,顺序与相应子计划一致。(这些标注是在PostgreSQL9.5 中新增的;在更早的版本中,读者必须通过检查子计划来推断目标表。)
EXPLAIN ANALYZE显示的Planning time,是从已解析的查询生成查询计划并完成优化所花费的时间;其中不包括解析和重写。
EXPLAIN ANALYZE显示的Execution time包括执行器启动和关闭所需的时间,以及运行所有已触发触发器所花费的时间,但不包括解析、重写和规划时间。若存在BEFORE触发器,其执行时间会计入相关的 Insert、Update 或 Delete 节点;而AFTER触发器的时间不会计入那里,因为AFTER触发器是在整个计划完成之后才触发的。每个触发器,无论是BEFORE还是AFTER,其总耗时也会单独显示。注意,延迟约束触发器要到事务结束时才会执行,因此EXPLAIN ANALYZE完全不会把它们计入其中。
EXPLAIN ANALYZE测得的运行时间,可能以两种重要方式偏离同一查询的正常执行时间。第一,由于不会向客户端传递任何输出行,因此网络传输开销和 I/O 转换开销均不会被计入。第二,EXPLAIN ANALYZE附加的测量开销本身可能相当可观,尤其是在操作系统调用gettimeofday()较慢的机器上。可以使用pg_test_timing工具来测量系统上的计时开销。
不应将EXPLAIN结果外推到与实际测试场景差异很大的情况。例如,在一个很小的表上得到的结果,不能假定也适用于大型表。规划器的代价估计不是线性的,因此它可能会为更大或更小的表选择不同的计划。一个极端示例是:对于只占用一个磁盘页的表,无论索引是否可用,几乎总会得到顺序扫描计划。规划器认识到,无论如何处理该表都需要一次磁盘页读取,因此再额外读取页面去查看索引并没有价值。(前面的polygon_tbl示例已经展示过这种情况。)
有时实际值与估计值不太一致,但实际上并没有问题。其中一种情况是,计划节点的执行因LIMIT或类似效果而提前停止。例如,在前面使用的LIMIT查询中:
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.29..14.71 rows=2 width=244) (actual time=0.177..0.249 rows=2 loops=1)
-> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..72.42 rows=10 width=244) (actual time=0.174..0.244 rows=2 loops=1)
Index Cond: (unique2 > 9000)
Filter: (unique1 < 100)
Rows Removed by Filter: 287
Planning time: 0.096 ms
Execution time: 0.336 ms
Index Scan 节点的估计代价和行数按该节点执行到完成的情况显示。但实际上,Limit 节点取得两行后就停止请求行,因此实际行数只有 2,运行时间也小于估计代价所暗示的时间。这不是估计错误,只是估计值与实际值的显示方式存在差别。
归并连接也会产生一些容易误导人的计量现象。如果归并连接已经耗尽其中一个输入,而另一个输入中的下一个键值又大于前一个输入的最后一个键值,那么它就会停止继续读取前者;在这种情况下,不可能再有更多匹配,因此也就不需要扫描另一个输入的剩余部分。这会导致某个子节点没有被完整读取,其结果与前面提到的LIMIT情况类似。此外,如果外侧(第一个)子节点包含带有重复键值的行,内侧(第二个)子节点会被回退并重新扫描,以查找能够匹配该键值的行。EXPLAIN ANALYZE会把这些对同一内侧行的重复输出,统计得像是真实的额外行一样。当外侧存在大量重复值时,内侧子计划节点报告出的实际行计数,可能会明显大于内侧关系中真实存在的行数。
由于实现上的限制,BitmapAnd 和 BitmapOr 节点总是报告其实际行计数为零。