EXPLAIN
#PostgreSQL 为它接收的每个查询设计一个查询计划。选择与查询结构和数据属性匹配的正确计划对于良好的性能至关重要,因此系统包含一个复杂的规划器,它尝试选择良好的计划。您可以使用EXPLAIN
命令来查看规划器为任何查询创建的查询计划。阅读计划是一门需要一些经验才能掌握的艺术,但本节尝试涵盖基础知识。
本节中的示例来自回归测试数据库,在执行VACUUM ANALYZE
之后,使用 v17 开发源代码。如果您自己尝试这些示例,应该能够获得类似的结果,但您的估计成本和行数可能会略有不同,因为ANALYZE
的统计数据是随机样本而不是精确值,而且成本本质上在某种程度上依赖于平台。
这些示例使用EXPLAIN
的默认“文本”输出格式,该格式紧凑且方便人类阅读。如果您想将EXPLAIN
的输出提供给程序进行进一步分析,则应改用其机器可读的输出格式之一(XML、JSON 或 YAML)。
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 计划节点的“过滤器”条件应用。这意味着计划节点会检查其扫描的每一行的条件,并且仅输出通过条件的行。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
,这样就不需要检索所有行,并且规划器改变了它的决定。请注意,索引扫描节点的总成本和行数显示为好像它已运行完成。然而,Limit 节点预计在仅检索了这些行数的五分之一后停止,因此它的总成本仅为五分之一,这就是查询的实际估计成本。此计划优于在之前的计划中添加 Limit 节点,因为 Limit 无法避免支付位图扫描的启动成本,因此使用该方法总成本将超过 25 个单位。
让我们尝试使用我们一直在讨论的列连接两个表
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; QUERY PLAN -------------------------------------------------------------------------------------- Nested Loop (cost=4.65..118.50 rows=10 width=488) -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) Recheck Cond: (unique1 < 10) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) Index Cond: (unique1 < 10) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) Index Cond: (unique2 = t1.unique2)
在这个计划中,我们有一个嵌套循环连接节点,其中有两个表扫描作为输入,或子节点。节点摘要行的缩进反映了计划树结构。连接的第一个,或“外部”子节点是类似于我们之前看到的位图扫描。它的成本和行数与我们从 SELECT ... WHERE unique1 < 10
中获得的一样,因为我们在该节点上应用了 WHERE
子句 unique1 < 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
索引中进行测试,因此它在连接节点应用。这减少了连接节点的估计输出行数,但不会更改任何一个输入扫描。
请注意,在此处,规划器已选择通过在其之上放置一个物化计划节点来“物化”连接的内部关系。这意味着 t2
索引扫描将仅执行一次,即使嵌套循环连接节点需要读取该数据十次,每次来自外部关系一行。物化节点在读取时将数据保存在内存中,然后在随后的每次传递中从内存返回数据。
当处理外连接时,您可能会看到连接计划节点附加了“连接过滤器”和普通的“过滤器”条件。连接过滤器条件来自外连接的 ON
子句,因此未能通过连接过滤器条件的行仍然可能作为空扩展行发出。但是,普通过滤器条件在外连接规则之后应用,因此用于无条件地删除行。在内连接中,这些类型的过滤器之间没有语义差异。
如果我们稍微改变查询的选择性,我们可能会得到一个非常不同的连接计划
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ Hash Join (cost=226.23..709.73 rows=100 width=488) Hash Cond: (t2.unique2 = t1.unique2) -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) -> Hash (cost=224.98..224.98 rows=100 width=244) -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) Recheck Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) Index Cond: (unique1 < 100)
在这里,规划器已选择使用哈希连接,其中将一个表的行输入到内存中的哈希表中,然后扫描另一个表,并探测哈希表以查找每行的匹配项。再次注意缩进如何反映计划结构:对 tenk1
的位图扫描是哈希节点的输入,该节点构造哈希表。然后将其返回到哈希连接节点,该节点从其外部子计划中读取行,并为每个行搜索哈希表。
另一种可能的连接类型是合并连接,在此处说明
EXPLAIN SELECT * FROM tenk1 t1, onek t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ Merge Join (cost=0.56..233.49 rows=10 width=488) Merge Cond: (t1.unique2 = t2.unique2) -> Index Scan using tenk1_unique2 on tenk1 t1 (cost=0.29..643.28 rows=100 width=244) Filter: (unique1 < 100) -> Index Scan using onek_unique2 on onek t2 (cost=0.28..166.28 rows=1000 width=244)
合并连接要求其输入数据按连接键排序。在此示例中,每个输入都通过使用索引扫描按正确的顺序访问行来排序;但是也可以使用顺序扫描和排序。(由于索引扫描所需的非顺序磁盘访问,顺序扫描和排序经常在排序许多行时胜过索引扫描。)
查看变体计划的一种方法是强制规划器忽略它认为最便宜的策略,使用第 19.7.1 节中描述的启用/禁用标志。(这是一个粗略的工具,但很有用。另请参阅第 14.3 节。)例如,如果我们不确定合并连接是上一个示例的最佳连接类型,我们可以尝试
SET enable_mergejoin = off; EXPLAIN SELECT * FROM tenk1 t1, onek t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ Hash Join (cost=226.23..344.08 rows=10 width=488) Hash Cond: (t2.unique2 = t1.unique2) -> Seq Scan on onek t2 (cost=0.00..114.00 rows=1000 width=244) -> Hash (cost=224.98..224.98 rows=100 width=244) -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) Recheck Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) Index Cond: (unique1 < 100)
这表明规划器认为,对于这种情况,哈希连接的成本将比合并连接高出近 50%。当然,下一个问题是它是否正确。我们可以使用 EXPLAIN ANALYZE
来调查,如下所述。
一些查询计划涉及子计划,这些子计划来自原始查询中的子 SELECT
。此类查询有时可以转换为普通的连接计划,但是当它们不能时,我们会得到如下计划
EXPLAIN VERBOSE SELECT unique1 FROM tenk1 t WHERE t.ten < ALL (SELECT o.ten FROM onek o WHERE o.four = t.four); QUERY PLAN ------------------------------------------------------------------------- Seq Scan on public.tenk1 t (cost=0.00..586095.00 rows=5000 width=4) Output: t.unique1 Filter: (ALL (t.ten < (SubPlan 1).col1)) SubPlan 1 -> Seq Scan on public.onek o (cost=0.00..116.50 rows=250 width=4) Output: o.ten Filter: (o.four = t.four)
这个相当人为的例子说明了几个要点:来自外部计划级别的值可以传递到子计划中(此处,t.four
被传递下来),并且子选择的结果可供外部计划使用。这些结果值由带有诸如 (
之类的表示法的 subplan_name
).colN
EXPLAIN
显示,该表示法指的是子 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
构造的情况截然不同。
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.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1) -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1) Recheck Cond: (unique1 < 10) Heap Blocks: exact=10 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1) Index Cond: (unique1 < 10) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10) Index Cond: (unique2 = t1.unique2) Planning Time: 0.485 ms Execution Time: 0.073 ms
请注意,“实际时间”值以实际时间的毫秒为单位,而 cost
估计值以任意单位表示;因此它们不太可能匹配。通常最重要的事情是要查看估计的行数是否与实际情况相当接近。在此示例中,所有估计都非常准确,但这在实践中很少见。
在某些查询计划中,子计划节点可能会执行多次。例如,内部索引扫描将在上述嵌套循环计划中为每个外部行执行一次。在这种情况下,loops
值报告节点的总执行次数,而显示的实际时间和行值是每次执行的平均值。这样做是为了使这些数字与成本估计的显示方式具有可比性。乘以 loops
值即可获得在该节点中实际花费的总时间。在上面的示例中,我们总共花费了 0.030 毫秒在 tenk2
上执行索引扫描。
在某些情况下,EXPLAIN ANALYZE
显示的执行统计信息超出了计划节点执行时间和行数。例如,排序和哈希节点提供额外的信息
EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------- Sort (cost=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100 loops=1) Sort Key: t1.fivethous Sort Method: quicksort Memory: 74kB -> Hash Join (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100 loops=1) Hash Cond: (t2.unique2 = t1.unique2) -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000 loops=1) -> Hash (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 35kB -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100 loops=1) Recheck Cond: (unique1 < 100) Heap Blocks: exact=90 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1) Index Cond: (unique1 < 100) Planning Time: 0.187 ms Execution Time: 3.036 ms
排序节点显示使用的排序方法(特别是排序是在内存中还是在磁盘上)以及所需的内存或磁盘空间量。哈希节点显示哈希桶和批次的数量以及用于哈希表的峰值内存量。(如果批次数超过一个,也将涉及磁盘空间的使用,但未显示。)
另一种额外信息类型是被过滤器条件删除的行数
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7; QUERY PLAN --------------------------------------------------------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000 loops=1) Filter: (ten < 7) Rows Removed by Filter: 3000 Planning Time: 0.102 ms Execution Time: 2.145 ms
这些计数对于在连接节点应用的过滤器条件特别有价值。只有当至少一个扫描的行(或连接节点情况下的潜在连接对)被过滤器条件拒绝时,才会显示“删除的行”行。
与过滤器条件类似的情况发生在“有损”索引扫描中。例如,考虑此搜索包含特定点的多边形
EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)'; QUERY PLAN ------------------------------------------------------------------------------------------------------ Seq Scan on polygon_tbl (cost=0.00..1.09 rows=1 width=85) (actual time=0.023..0.023 rows=0 loops=1) Filter: (f1 @> '((0.5,2))'::polygon) Rows Removed by Filter: 7 Planning Time: 0.039 ms Execution Time: 0.033 ms
规划器认为(完全正确)此示例表太小,不值得使用索引扫描,因此我们进行了普通的顺序扫描,其中所有行都被过滤器条件拒绝。但是,如果我们强制使用索引扫描,我们会看到
SET enable_seqscan TO off; EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------- Index Scan using gpolygonind on polygon_tbl (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1) Index Cond: (f1 @> '((0.5,2))'::polygon) Rows Removed by Index Recheck: 1 Planning Time: 0.039 ms Execution Time: 0.098 ms
这里我们可以看到,索引返回了一个候选行,然后该行被索引条件的重新检查拒绝了。发生这种情况是因为 GiST 索引对于多边形包含测试是“有损的”:它实际上返回的是与目标重叠的多边形所在的行,然后我们必须对这些行执行精确的包含测试。
EXPLAIN
有一个 BUFFERS
选项,可以与 ANALYZE
一起使用,以获取更多的运行时统计信息。
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=25.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10 loops=1) Recheck Cond: ((unique1 < 100) AND (unique2 > 9000)) Heap Blocks: exact=10 Buffers: shared hit=14 read=3 -> BitmapAnd (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1) Buffers: shared hit=4 read=3 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1) Index Cond: (unique1 < 100) Buffers: shared hit=2 -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1) Index Cond: (unique2 > 9000) Buffers: shared hit=2 read=3 Planning: Buffers: shared hit=3 Planning Time: 0.162 ms Execution Time: 0.143 ms
BUFFERS
提供的数据有助于识别查询中 I/O 密集程度最高的部分。
请记住,由于 EXPLAIN ANALYZE
实际上会运行查询,因此任何副作用都会像往常一样发生,即使查询可能输出的任何结果都会被丢弃,而转而打印 EXPLAIN
数据。 如果你想分析一个修改数据的查询,而不改变你的表,你可以事后回滚该命令,例如:
BEGIN; EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Update on tenk1 (cost=5.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0 loops=1) -> Bitmap Heap Scan on tenk1 (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100 loops=1) Recheck Cond: (unique1 < 100) Heap Blocks: exact=90 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1) Index Cond: (unique1 < 100) Planning Time: 0.151 ms Execution Time: 1.856 ms ROLLBACK;
如本例所示,当查询是 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
是从解析的查询生成查询计划并对其进行优化所花费的时间。它不包括解析或重写。
EXPLAIN ANALYZE
显示的 Execution time
包括执行器启动和关闭时间,以及运行任何触发器的时间,但不包括解析、重写或计划时间。执行任何 BEFORE
触发器所花费的时间包含在相关的 Insert、Update 或 Delete 节点的时间内;但是执行 AFTER
触发器所花费的时间不计入此处,因为 AFTER
触发器是在整个计划完成后触发的。每个触发器(无论是 BEFORE
还是 AFTER
)中花费的总时间也会单独显示。 请注意,延迟约束触发器将不会执行,直到事务结束,因此 EXPLAIN ANALYZE
完全不会考虑它们。
顶层节点显示的时间不包括将查询的输出数据转换为可显示形式或将其发送到客户端所需的任何时间。 虽然 EXPLAIN ANALYZE
永远不会将数据发送到客户端,但可以通过指定 SERIALIZE
选项来告诉它将查询的输出数据转换为可显示的形式并测量所需的时间。该时间将单独显示,并且也包含在总的 Execution time
中。
由 EXPLAIN ANALYZE
测量的运行时间与同一查询的正常执行相比,在两个方面存在显著差异。 首先,由于没有输出行传递到客户端,因此不包括网络传输成本。 除非指定 SERIALIZE
,否则也不包括 I/O 转换成本。 其次,EXPLAIN ANALYZE
添加的测量开销可能很大,尤其是在 gettimeofday()
操作系统调用速度较慢的机器上。 你可以使用 pg_test_timing 工具来测量系统上计时的开销。
EXPLAIN
的结果不应外推到与你实际测试的情况大相径庭的情况; 例如,不能假设玩具大小的表的结果适用于大型表。 规划器的成本估算不是线性的,因此它可能会为更大或更小的表选择不同的计划。一个极端的例子是,在仅占用一个磁盘页面的表上,无论索引是否可用,你几乎总是会得到顺序扫描计划。 规划器意识到无论如何处理该表都将需要读取一个磁盘页面,因此花费额外的页面读取来查看索引没有任何价值。(我们在上面的 polygon_tbl
示例中看到了这种情况。)
在某些情况下,实际值和估计值可能无法很好地匹配,但实际上没有任何问题。一种情况是,计划节点执行因 LIMIT
或类似效果而提前停止。 例如,在之前使用的 LIMIT
查询中,
EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1) -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1) Index Cond: (unique2 > 9000) Filter: (unique1 < 100) Rows Removed by Filter: 287 Planning Time: 0.077 ms Execution Time: 0.086 ms
Index Scan 节点的估计成本和行数显示为好像它已运行完成。但实际上,Limit 节点在获取两个行后停止请求行,因此实际行数仅为 2,并且运行时间少于成本估计所暗示的时间。 这不是估计错误,只是估计值和真实值的显示方式存在差异。
合并连接也具有可能会迷惑粗心人士的测量伪像。 如果合并连接的一个输入耗尽了另一个输入,并且一个输入中的下一个键值大于另一个输入的最后一个键值,则它将停止读取一个输入; 在这种情况下,可能不会再有匹配项,因此无需扫描第一个输入的其余部分。 这导致不读取一个子项的所有内容,其结果与 LIMIT
提到的结果类似。 此外,如果外部(第一个)子项包含具有重复键值的行,则会备份内部(第二个)子项并重新扫描其与该键值匹配的部分行。 EXPLAIN ANALYZE
会将这些重复发出的相同内部行计为好像它们是真正的附加行。当存在许多外部重复项时,内部子计划节点报告的实际行数可能显著大于实际位于内部关系中的行数。
由于实现限制,BitmapAnd 和 BitmapOr 节点始终将其实际行数报告为零。
通常, EXPLAIN
将显示规划器创建的每个计划节点。 但是,在某些情况下,执行器可以根据规划时不可用的参数值确定某些节点不需要执行,因为它们无法生成任何行。(目前,这种情况仅发生在正在扫描分区表的 Append 或 MergeAppend 节点的子节点上。)发生这种情况时,这些计划节点将从 EXPLAIN
输出中省略,并显示 Subplans Removed:
注释。N
如果你在文档中发现任何不正确、与你使用特定功能的经验不符或需要进一步澄清的内容,请使用此表单报告文档问题。