14.1. 使用 EXPLAIN
PostgreSQL 会为收到的每个查询制定一个查询计划。要获得良好的性能,必须让计划适合查询结构与数据特点,因此系统包含一个复杂的规划器,尝试选择合适的计划。可以使用 EXPLAIN 命令查看规划器为任意查询创建的计划。读懂计划是一门需要经验的技艺,本节将介绍它的基础。
本节示例使用 v18 开发版源代码,在执行 VACUUM ANALYZE 后的回归测试数据库上取得。自行尝试时应能得到类似结果,但估算成本与行数可能略有不同:ANALYZE 的统计来自随机样本而非精确统计,成本本身也在一定程度上依赖平台。
示例使用 EXPLAIN 默认的 text 输出格式,它紧凑且方便人阅读。如果要把输出交给程序继续分析,应改用 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 行,因此成本并未降低,反而略微增加,以反映检查条件需要的额外 CPU 时间;精确地说,增加了 10000 × cpu_operator_cost。
此查询实际选中 7000 行,但 rows 只是近似估计。重复实验时可能得到略有不同的估算;每次执行 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' 降低了估算输出行数,却没有降低成本,因为仍要访问同一组行。这个索引只包含 unique1 列,不能把 stringu1 条件作为索引条件,只能对通过索引取回的行进行过滤。因此成本还会略微增加,反映这项额外检查。
有些情况下,规划器会偏好简单的索引扫描计划:
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 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 节点,因为 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 相同,因为 unique1 < 10 这个 WHERE 条件在该节点应用。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 子句,所以不满足它的行仍可能以补充 NULL 的形式输出。普通 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)
归并连接要求输入按连接键有序。本例中,两个输入各自通过索引扫描按正确顺序访问行;也可以采用顺序扫描再排序。对大量行排序时,顺序扫描加排序经常优于索引扫描,因为索引扫描需要非顺序的磁盘访问。
查看不同方案的一种办法,是用第 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 调查。
使用启用/禁用标志禁用某类计划节点时,许多标志只是使规划器尽量避免采用它,并不彻底禁止。这是有意的设计,确保规划器仍有能力为给定查询构造计划。如果最终计划仍包含已禁用的节点,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 除了不引用外层变量之外,还至多返回一行,就可能作为初始化计划(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
每次外层计划执行时,初始化计划只执行一次,结果被保存,供外层后续各行复用。因此本例中 random() 只求值一次,所有 t1.ten 都与同一个随机整数比较;这与没有子 SELECT 结构时的情况很不同。
14.1.2. EXPLAIN ANALYZE
使用 EXPLAIN 的 ANALYZE 选项,可以检查规划器估算是否准确。此选项会真正执行查询,随后同时显示每个计划节点累积的真实行数、真实运行时间,以及普通 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 报告节点的总执行次数,显示的实际时间与行数则是每次执行的平均值,以便与成本估算的表达方式比较。把平均时间乘以 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 查询的变体只执行了一次索引搜索。与原查询相比,它遍历索引的时间更少,因为 IN 中的值对应着相邻的索引元组,这些元组位于 tenk1_thous_tenthous 的同一个叶页。
对使用跳跃扫描(skip scan)优化、从而更高效遍历索引的 B-tree 扫描,Index Searches 行也很有用:
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 表的 four、unique1 两列索引。扫描进行了 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;不希望如此时,可以显式关闭:
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;
当查询是 INSERT、UPDATE、DELETE 或 MERGE 时,真正应用表修改的工作由顶层 Insert、Update、Delete 或 Merge 节点完成。下层节点则定位旧行、计算新数据,或两者兼做。因此上例中,熟悉的位图表扫描把输出交给 Update 节点,由后者保存更新后的行。修改数据的节点可能消耗大量运行时间,本例甚至占了绝大部分时间,但规划器目前不会把这项工作加进成本估算,因为所有正确计划都需要完成同样的修改,不影响计划选择。
当 UPDATE、DELETE 或 MERGE 影响分区表或继承层级时,输出可能如下:
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 是从已经解析的查询生成并优化查询计划所需的时间,不包括解析或重写。
Execution time 包含执行器启动、关闭,以及运行触发器的时间,但不包括解析、重写或规划。BEFORE 触发器的耗时计入相应 Insert、Update 或 Delete 节点;AFTER 触发器要等整个计划完成后才触发,因此不计入这些节点。每个触发器的总耗时,无论 BEFORE 还是 AFTER,也会单独显示。延迟约束触发器到事务结束才执行,所以 EXPLAIN ANALYZE 完全不统计它们。
顶层节点显示的时间不包含把查询输出转换为可显示形式,或发送到客户端所需的时间。EXPLAIN ANALYZE 本身永远不会把查询数据发给客户端,但可以指定 SERIALIZE,让它把输出转换为可显示形式并测量耗时。这个时间会单独显示,也计入总 Execution time。
14.1.3. 注意事项
EXPLAIN ANALYZE 测得的运行时间,主要有两方面会偏离同一查询的正常执行时间。第一,它不向客户端发送结果行,因此不统计网络传输成本;除非指定 SERIALIZE,也不统计 I/O 转换成本。第二,它附加的测量开销可能很大,尤其在操作系统的 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 注记。
原文与版权:The PostgreSQL Global Development Group,14.1. Using EXPLAIN。本稿将当前官方章节完整汉化,保留全部 SQL 与计划输出;删除站点导航、目录和索引锚点。示例沿用原文的 v18 开发版回归测试数据,展示的成本和时间属于原文示例,本批未运行数据库。按 PostgreSQL License 使用,版权及许可如下。
PostgreSQL Database Management System
(also known as Postgres, formerly as Postgres95)
Portions Copyright © 1996-2026, The PostgreSQL Global Development Group
Portions Copyright © 1994, The Regents of the University of California
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose, without fee, and without a written agreement
is hereby granted, provided that the above copyright notice and this
paragraph and the following two paragraphs appear in all copies.
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
ON AN “AS IS” BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.











暂无评论内容