一、使用EXPLAIN
PostgreSQL为每个收到查询产生一个查询计划。 选择正确的计划来匹配查询结构和数据的属性对于好的性能来说绝对是最关键的,因此系统包含了一个复杂的规划器来尝试选择好的计划。 你可以使用EXPLAIN命令察看规划器为任何查询生成的查询计划。 阅读查询计划是一门艺术,它要求一些经验来掌握,但是本节只试图覆盖一些基础。
本节中的例子都是从开发源代码的回归测试数据库中抽取出来的,并且在此之前做过一次VACUUM ANALYZE。你应该能够在自己尝试这些例子时得到相似的结果,但是你的估计代价和行计数可能会小幅变化,因为ANALYZE的统计信息是随机采样而不是精确值,并且代价也与平台有某种程度的相关性。
这些例子使用EXPLAIN的默认“text”输出格式,这种格式紧凑并且便于人类阅读。如果你想把EXPLAIN的输出交给一个程序做进一步分析,你应该使用它的某种机器可读的输出格式(XML、JSON 或 YAML)。
1.1 EXPAIN 基础
查询计划的结构是一个计划结点的树。最底层的结点是扫描结点:它们从表中返回未经处理的行。 不同的表访问模式有不同的扫描结点类型:顺序扫描、索引扫描、位图索引扫描。 也还有不是表的行来源,例如VALUES子句和FROM中返回集合的函数,它们有自己的结点类型。如果查询需要连接、聚集、排序、或者在未经处理的行上的其它操作,那么就会在扫描结点之上有其它额外的结点来执行这些操作。 并且,做这些操作通常都有多种方法,因此在这些位置也有可能出现不同的结点类型。 EXPLAIN给计划树中每个结点都输出一行,显示基本的结点类型和计划器为该计划结点的执行所做的开销估计。 第一行(最上层的结点)是对该计划的总执行开销的估计;计划器试图最小化的就是这个数字。
这里是一个简单的例子,只是用来显示输出看起来是什么样的:
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
由于这个查询没有WHERE子句,它必须扫描表中的所有行,因此计划器只能选择使用一个简单的顺序扫描计划。被包含在圆括号中的数字是(从左至右):
-
估计的启动开销。在输出阶段可以开始之前消耗的时间,例如在一个排序结点里执行排序的时间。
-
估计的总开销。这个估计值基于的假设是计划结点会被运行到完成,即所有可用的行都被检索。不过实际上一个结点的父结点可能很快停止读所有可用的行(见下面的
LIMIT例子)。 -
这个计划结点输出行数的估计值。同样,也假定该结点能运行到完成。
-
预计这个计划结点输出的行平均宽度(以字节计算)。
开销是用规划器的开销参数所决定的捏造单位来衡量的。传统上以取磁盘页面为单位来度量开销; 也就是seq_page_cost将被按照习惯设为1.0,其它开销参数将相对于它来设置。 本节的例子都假定这些参数使用默认值。
有一点很重要:一个上层结点的开销包括它的所有子结点的开销。还有一点也很重要:这个开销只反映规划器关心的东西。特别是这个开销没有考虑结果行传递给客户端所花费的时间,这个时间可能是实际花费时间中的一个重要因素;但是它被规划器忽略了,因为它无法通过修改计划来改变(我们相信,每个正确的计划都将输出同样的行集)。
行数值有一些小技巧,因为它不是计划结点处理或扫描过的行数,而是该结点发出的行数。这通常比被扫描的行数少一些, 因为有些被扫描的行会被应用于此结点上的任意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有358个磁盘页面和10000行。 开销被计算为 (页面读取数*seq_page_cost)+(扫描的行数*cpu_tuple_cost)。默认情况下,seq_page_cost是1.0,cpu_tuple_cost是0.01, 因此估计的开销是 (358 * 1.0) + (10000 * 0.01) = 458。
现在让我们修改查询并增加一个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子句被当做一个“过滤器”条件附加到顺序扫描计划结点。 这意味着该计划结点为它扫描的每一行检查该条件,并且只输出通过该条件的行。因为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列上。 它被用来过滤从索引中检索出的行。因此开销实际上略微增加了一些以反映这个额外的检查。
在某些情况下规划器将更倾向于一个“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。
如果在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这样不是所有的行都需要被检索,并且规划器改变了它的决定。注意索引扫描结点的总开销和行计数显示出好像它会被运行到完成。但是,限制结点在检索到这些行的五分之一后就会停止,因此它的总开销只是索引扫描结点的五分之一,并且这是查询的实际估计开销。之所以用这个计划而不是在之前的计划上增加一个限制结点是因为限制无法避免在位图扫描上花费启动开销,因此总开销会是超过那种方法(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)
在这个计划中,我们有一个嵌套循环连接结点,它有两个表扫描作为输入或子结点。该结点的摘要行的缩进反映了计划树的结构。连接的第一个(或“outer”)子结点是一个与前面见

本文围绕PostgreSQL展开,介绍了使用EXPLAIN查看查询计划,包括基础用法和ANALYZE选项以检查估计值准确性;阐述规划器使用的统计信息;说明了用显示JOIN子句控制规划器的方法;还给出填充数据库的高效建议,如禁用自动提交、使用COPY等,最后提及非持久设置提升性能。
最低0.47元/天 解锁文章
3617

被折叠的 条评论
为什么被折叠?



