EXPLAIN
#PostgreSQL 为接收到的每个查询制定一个查询计划。选择与查询结构和数据属性匹配的正确计划对于性能来说至关重要,因此系统包括一个复杂的计划程序,该计划程序尝试选择良好的计划。您可以使用EXPLAIN
命令来查看计划程序为任何查询创建的查询计划。计划阅读是一门需要一些经验才能掌握的艺术,但本节尝试涵盖基础知识。
本节中的示例取自于对VACUUM ANALYZE
进行操作后的回归测试数据库,使用 v17 开发源。当您尝试执行示例时,您应该可以获得类似的结果,但您的估计成本和行数可能略有不同,因为ANALYZE
的统计信息是随机抽样而不是准确信息,并且成本本质上在一定程度上取决于平台。
这些示例使用EXPLAIN
的默认“text” 输出格式,该格式简洁方便人类阅读。如果您想将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)
这里,计划器决定使用两步计划:子计划节点访问索引以查找与索引条件匹配的行的位置,然后上层计划节点实际从表本身获取这些行。单独获取行比顺序读取要昂贵得多,但由于不必访问表的全部页面,因此这仍然比顺序扫描要便宜。(使用两个计划级别的原因是,上层计划节点在读取行位置之前,将由索引确定的行位置按物理顺序进行排序,以尽量减少单独获取的成本。“bitmap” 在节点名称中提到是执行排序的机制。)
现在,让我们向 WHERE
子句添加另一个条件
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx'; QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on tenk1 (cost=5.04..225.20 rows=1 width=244) Recheck Cond: (unique1 < 100) Filter: (stringu1 = 'xxx'::name) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) Index Cond: (unique1 < 100)
添加的条件 stringu1 = 'xxx'
减少了输出行数的估计值,但没有减少成本,因为我们仍然必须访问相同的一组行。这是因为 stringu1
子句不能应用为索引条件,因为此索引仅在 unique1
列上。相反,它作为使用该索引获取的行上的筛选器应用。因此,成本实际上略有增加,以反映此额外检查。
在某些情况下,计划器会更喜欢 “simple” 索引扫描计划
EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42; QUERY PLAN ----------------------------------------------------------------------------- Index Scan using tenk1_unique1 on tenk1 (cost=0.29..8.30 rows=1 width=244) Index Cond: (unique1 = 42)
在这种计划类型中,表行以索引顺序获取,这样读取它们的成本会更高,但行位置的排序成本却非常低,不值得这样做。对于只获取一行查询,你将看到此计划类型。对于具有与索引顺序匹配 ORDER BY
条件的查询,也经常使用此计划,因为此时无需额外的排序步骤来满足 ORDER BY
。在此示例中,添加 ORDER BY unique1
会使用相同的计划,因为索引已隐式提供请求的排序。
规划器可以多种方式执行 ORDER BY
子句。以上示例显示了如何隐式完成此排序子句。规划器还可以添加一个明确的 Sort
步骤
EXPLAIN SELECT * FROM tenk1 ORDER BY unique1; QUERY PLAN ------------------------------------------------------------------- Sort (cost=1109.39..1134.39 rows=10000 width=244) Sort Key: unique1 -> Seq Scan on tenk1 (cost=0.00..445.00 rows=10000 width=244)
如果计划的一部分保证按照所需的排序键前缀对排序,则规划器可能决定使用 Incremental Sort
步骤
EXPLAIN SELECT * FROM tenk1 ORDER BY hundred, ten LIMIT 100; QUERY PLAN ------------------------------------------------------------------------------------------------ Limit (cost=19.35..39.49 rows=100 width=244) -> Incremental Sort (cost=19.35..2033.39 rows=10000 width=244) Sort Key: hundred, ten Presorted Key: hundred -> Index Scan using tenk1_hundred on tenk1 (cost=0.29..1574.20 rows=10000 width=244)
与常规排序相比,增量排序允许在对整个结果集进行排序之前返回元组,此功能特别能够使用 LIMIT
查询进行优化。此外,还可以减少内存使用量并降低将排序数据溢出到磁盘的可能性,但代价是将结果集拆分为多个排序批次,从而增加了开销。
如果在 WHERE
中引用的多个列有单独的索引,规划器可能会选择使用索引的 AND 或 OR 组合
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=25.07..60.11 rows=10 width=244) Recheck Cond: ((unique1 < 100) AND (unique2 > 9000)) -> BitmapAnd (cost=25.07..25.07 rows=10 width=0) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) Index Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) Index Cond: (unique2 > 9000)
但这需要访问两个索引,因此与只使用一个索引并将另一条件作为过滤器相比,它不一定会有所帮助。如果你改变涉及的范围,你将看到计划会相应改变。
以下是一个展示 LIMIT
影响的示例
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2; QUERY PLAN ------------------------------------------------------------------------------------- Limit (cost=0.29..14.28 rows=2 width=244) -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..70.27 rows=10 width=244) Index Cond: (unique2 > 9000) Filter: (unique1 < 100)
这是与上述查询相同的查询,但我们添加了一个 LIMIT
,这样不必检索所有行,并且计划程序将改变其主意,考虑做什么。注意,索引扫描节点的总成本和行计数显示为如果运行到完成的情况。然而,Limit 节点预计在仅检索到其五分之一的行后停止,因此它的总成本仅为五分之一,并且这是查询的实际估计成本。该方案优于向以前的方案添加 Limit 节点,因为 Limit 无法避免支付位图扫描的启动成本,因此使用该方法的总成本将超过 25 个单位。
让我们尝试连接两个表,使用我们一直在讨论的列
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; QUERY PLAN -------------------------------------------------------------------------------------- Nested Loop (cost=4.65..118.50 rows=10 width=488) -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) Recheck Cond: (unique1 < 10) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) Index Cond: (unique1 < 10) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) Index Cond: (unique2 = t1.unique2)
在此计划中,我们有一个嵌套循环连接节点,有两个表扫描作为输入或子级。节点摘要行的缩进反映了计划树结构。连接的第一个或 “outer” 子级是类似于我们在之前看到的位图扫描。它的成本和行计数与我们从 SELECT ... WHERE unique1 < 10
处获得的一样,因为我们在该节点应用了 WHERE
子句 unique1 < 10
。t1.unique2 = t2.unique2
子句尚不相关,因此它不会影响外部扫描的行计数。嵌套循环连接节点将针对从外部子级获得的每一行运行其第二个或 “inner” 子级一次。当前外部行中的列值可插入到内部扫描中;这里,可以得到来自外部行的 t1.unique2
值,因此我们得到了一个计划和一个成本,这个计划和成本类似于我们在上面看到的简单 SELECT ... WHERE t2.unique2 =
案例。(估计成本实际上比上面所看到的低一些,这是由于预期在 constant
t2
上重复进行索引扫描期间出现的缓存。)然后,循环节点的成本将基于以下内容设置:外部扫描的成本,外加针对每一行(此处为 10 * 7.90)的内部扫描重复执行一次,外加一些用于处理连接的 CPU 时间。
在此示例中,联接的输出行数与两个扫描的行数的乘积相同,但这并非在所有情况下都成立,因为可能存在另外提到了这两个表的 WHERE
子句,因此只能在联接点应用,不能应用于任何输入扫描。这是一个示例
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred; QUERY PLAN --------------------------------------------------------------------------------------------- Nested Loop (cost=4.65..49.36 rows=33 width=488) Join Filter: (t1.hundred < t2.hundred) -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) Recheck Cond: (unique1 < 10) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) Index Cond: (unique1 < 10) -> Materialize (cost=0.29..8.51 rows=10 width=244) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..8.46 rows=10 width=244) Index Cond: (unique2 < 10)
条件 t1.hundred < t2.hundred
无法在 tenk2_unique2
索引中进行测试,因此其在联接节点处应用。这减少了联接节点的估计输出行数,但不会更改任何输入扫描。
请注意,在此处,规划器已选择通过在其上面放置 Materialize 计划节点来““序列化””联接的内部关系。这意味着 t2
索引扫描将仅执行一次,即使嵌套循环联接节点需要读取该数据十次,针对外部关系中的每一行一次。Materialize 节点在读取数据时将其保存在内存中,然后在随后的每一次传递中从内存中返回数据。
在处理外部联接时,你可能会看到同时具有 “联接筛选器” 和普通 “筛选器” 条件的联接计划节点。联接筛选器条件来自外部联接的 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
的位图扫描是 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
来调查这个问题,详见 下面。
某些查询计划涉及 子计划,这些子计划来自原始查询中的子 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
显示,带有类似于 (
的符号,它表示子 子计划名
).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
构造的情况下会发生的情况截然不同。
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
请注意,“实际时间” 的值以 real 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 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
Sort 节点显示所使用的排序方法(尤其是排序是否在内存中或磁盘上)以及所需的内存量或磁盘空间量。Hash 节点显示哈希桶和批处理的数量,以及用于哈希表的峰值内存量。(如果批处理的数量超过一个,那么还将涉及磁盘空间使用,但这没有显示出来。)
另一类型的额外信息是过滤条件所移除的行数
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 节点以显示将更新的特定目标表,其顺序与相应的子计划相同。
“Planning time
”(计划时间)通过 EXPLAIN ANALYZE
显示,这是从已解析查询生成查询计划并优化该计划所需的时间。它不包括解析或重写时间。
“Execution time
”(执行时间)通过 EXPLAIN ANALYZE
显示,它包括执行器启动和关闭时间以及运行触发的任何时间,但不包括解析、重写或计划时间。执行 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
索引扫描节点的估计成本和行计数显示为已运行完成。但实际上,在获得两个行之后,Limit 节点已停止请求行,因此实际行计数仅为 2,并且运行时间低于成本估算所建议的时间。这不是一个估算错误,只是估算值和真实值显示方式上存在差异。
合并联接还有一些可能让粗心的人混淆的计量工件。如果一个合并联接耗尽了另一个输入,并且另一个输入中的下一个键值大于另一个输入的最后键值,则合并联接将停止读取一个输入;在这种情况下,不会有更多匹配项,因此无需扫描第一个输入中的剩余部分。这会导致未读取某个子项的全部,产生类似于 LIMIT
所述的结果。此外,如果外部(第一个)子项包含键值重复的行,则内部(第二个)子项将被备份并针对其与键值匹配的行部分重新扫描。 EXPLAIN ANALYZE
会将这些内部行重复发出的情况计为额外的实际行。当外部重复项很多时,内部子计划节点报告的实际行计数可能显著大于内部关系中实际的行数。
由于实现限制,BitmapAnd 和 BitmapOr 节点始终将它们的实际行计数报告为零。
通常,EXPLAIN
将显示计划程序创建的每个计划节点。但是,在某些情况下,执行程序可以确定基于规划时不可用的参数值,某些节点不需要执行,因为它们不会产生任何行。(目前,这种情况只能发生在正在扫描分区表的 Append 或 MergeAppend 节点的子节点中。)当这种情况发生时,这些计划节点将从 EXPLAIN
输出中省略,而 已移除子计划:
批注将替代显示。N