pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
目录
查询性能可能受许多因素影响。其中一些因素可以由用户控制,另一些则属于系统底层设计的基本特性。本章提供一些帮助理解和调优PostgreSQL性能的提示。
EXPLAIN #PostgreSQL会为收到的每个查询制定一个查询计划。选择与查询结构和数据特性相匹配的正确计划,对获得良好的性能绝对至关重要。可以使用EXPLAIN命令查看系统为任意查询生成的查询计划。读懂计划是一门值得专门教程详述的技艺,本节并非那样的教程,但这里会介绍一些基础知识。
EXPLAIN当前引用的数字有:
估计启动代价(在输出扫描开始之前消耗的时间,例如排序节点中完成排序所需的时间)
估计总代价(如果所有行都被检索;不过它们也可能不被检索:例如带LIMIT子句的查询不会付出全部总代价。)
该计划节点输出的估计行数(同样,仅在完全执行的情况下)
该计划节点输出行的估计平均宽度(以字节计)
这些开销以磁盘页读取次数为单位来衡量。(CPU 工作量的估计会使用一些相当任意的经验系数转换成磁盘页单位。如果你想试验这些系数,请看第 16.4.5.2 节中的运行时配置参数列表。)
需要理解的一点是,上层节点的开销包含了其所有子节点的开销。还要注意,这个开销只反映规划器/优化器关心的内容。特别是,它没有考虑将结果行传输给前端所消耗的时间,而这在实际耗时中可能是相当主要的因素;但规划器会忽略这些代价,因为它无法通过改变计划来影响它们。(我们相信每个正确的计划都会输出相同的行集。)
行数输出(rows)这个数字有些容易误解,因为它不是查询处理/扫描的行数,它通常更小,反映的是在该节点上应用的任何WHERE子句条件的估计选择率。理想情况下,顶层的行数估计应当接近查询实际返回、更新或删除的行数。
下面是一些例子(使用执行过VACUUM ANALYZE的回归测试数据库以及 7.3 开发版本源码):
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..333.00 rows=10000 width=148)
这些数字的来源非常直接。如果执行:
SELECT * FROM pg_class WHERE relname = 'tenk1';
可以看到tenk1有 233 个磁盘页和 10000 行。因此估计代价为 233 次页读取(每次定义为代价 1.0),再加上 10000 * cpu_tuple_cost(当前为 0.01,可尝试SHOW cpu_tuple_cost)。
现在修改查询,添加一个WHERE条件:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;
QUERY PLAN
------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..358.00 rows=1033 width=148)
Filter: (unique1 < 1000)
由于有了WHERE子句,估计输出行数减少了。但扫描仍需访问全部 10000 行,因此代价没有下降;实际上还略有上升,以反映检查WHERE条件所消耗的额外 CPU 时间。
这条查询实际选出的行数是 1000,但行数估计只是近似值。如果重复这个实验,很可能会得到略有不同的估计值。此外,由于ANALYZE生成的统计信息来自该表的随机采样,这个估计值可能在每次执行ANALYZE之后发生变化。
再进一步收紧查询的条件:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 50;
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..179.33 rows=49 width=148)
Index Cond: (unique1 < 50)
可以看到,如果我们让WHERE条件的选择性足够强,规划器最终会认定索引扫描比顺序扫描更便宜。得益于索引,这个计划只需访问 50 行,因此尽管每次单独读取的代价高于顺序读取一整个磁盘页,它仍然是赢家。
再向WHERE子句添加另一个条件:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 50 AND stringu1 = 'xxx';
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..179.45 rows=1 width=148)
Index Cond: (unique1 < 50)
Filter: (stringu1 = 'xxx'::name)
新增的条件stringu1 = 'xxx'降低了估计输出行数,却没有降低代价,因为仍然必须访问同一组行。注意,stringu1子句不能用作索引条件(因为该索引只建立在unique1列上)。它只能作为过滤条件,应用到通过索引取出的行上。因此,代价实际上略有增加,以反映这项额外检查。
使用之前讨论的列,尝试连接两个表:
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------------------
Nested Loop (cost=0.00..327.02 rows=49 width=296)
-> Index Scan using tenk1_unique1 on tenk1 t1
(cost=0.00..179.33 rows=49 width=148)
Index Cond: (unique1 < 50)
-> Index Scan using tenk2_unique2 on tenk2 t2
(cost=0.00..3.01 rows=1 width=148)
Index Cond: ("outer".unique2 = t2.unique2)
在这个嵌套循环连接中,外层扫描与倒数第二个例子中的索引扫描相同,其代价和行计数也相同,因为WHERE子句unique1 < 50正是在该节点上应用的。t1.unique2 = t2.unique2子句此时还无关,因此不会影响外层扫描的行计数。对于内层(下层)扫描,当前外层扫描行的unique2值被代入内层索引扫描,产生一个形如t2.unique2 = 的索引条件。因此我们得到的内层扫描计划和代价,与执行constantEXPLAIN SELECT * FROM tenk2 WHERE unique2 = 42之类查询所得到的相同。循环节点的代价则建立在外层扫描代价之上,再加上每个外层行都要执行一次内侧扫描的代价(这里是 49 * 3.01),以及少量连接处理的 CPU 时间。
本例中,连接的输出行数等于两个扫描行数的乘积,但并非所有情况都如此,因为可能还有同时涉及两个表的WHERE子句,因此只能在连接处应用,不能应用到任一输入扫描。例如,如果我们加上WHERE ... AND t1.hundred < t2.hundred,它会减少连接节点的输出行数,但不会改变任何一个输入扫描。
查看不同计划的一种办法,是为每种计划类型设置启用/禁用开关,强制规划器放弃它认为最廉价的策略。(这是一种粗糙的工具,但很有用。另见第 13.3 节。)
SET enable_nestloop = off;
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;
QUERY PLAN
--------------------------------------------------------------------------
Hash Join (cost=179.45..563.06 rows=49 width=296)
Hash Cond: (t2.unique2 = t1.unique2)
-> Seq Scan on tenk2 t2 (cost=0.00..333.00 rows=10000 width=148)
-> Hash (cost=179.33..179.33 rows=49 width=148)
-> Index Scan using tenk1_unique1 on tenk1 t1
(cost=0.00..179.33 rows=49 width=148)
Index Cond: (unique1 < 50)
这个计划打算还是用那个熟悉的索引扫描取出tenk1中 50 行令人感兴趣的行,把它们存入一个内存中的哈希表,然后对tenk2做顺序扫描,对每一行tenk2在哈希表中探测t1.unique2 = t2.unique2的可能匹配。读取tenk1并建立哈希表的代价是哈希连接的启动代价,因为在开始读取tenk2之前不会有任何输出。连接的总时间估计中还包含一笔不小的 CPU 时间开销,用于对哈希表探测 10000 次。但注意,我们不是按 10000 乘以 179.33 来计费的;在这种计划类型中,哈希表的建立只做一次。
可以使用EXPLAIN ANALYZE检查规划器代价估计的准确性。这个命令会实际执行查询,然后显示每个计划节点内累计的真实运行时间,以及普通EXPLAIN所显示的相同估计代价。例如,我们可能得到这样的结果:
EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;
QUERY PLAN
-------------------------------------------------------------------------------
Nested Loop (cost=0.00..327.02 rows=49 width=296)
(actual time=1.181..29.822 rows=50 loops=1)
-> Index Scan using tenk1_unique1 on tenk1 t1
(cost=0.00..179.33 rows=49 width=148)
(actual time=0.630..8.917 rows=50 loops=1)
Index Cond: (unique1 < 50)
-> Index Scan using tenk2_unique2 on tenk2 t2
(cost=0.00..3.01 rows=1 width=148)
(actual time=0.295..0.324 rows=1 loops=50)
Total runtime: 31.604 ms
注意,“actual time”值以真实时间的毫秒为单位,而“cost”估计则以磁盘读取次数这样的任意单位表示,因此两者不太可能相互吻合。需要关注的是比值。
在某些查询计划中,一个子计划节点可能会执行多次。例如,上面那个嵌套循环计划中的内侧索引扫描会对外侧的每一行执行一次。在这种情况下,“loops”值报告的是该节点的总执行次数,而显示的 actual time 和 rows 值是每次执行的平均值。这样做是为了让这些数字更容易与代价估计的展示方式相比较。将它们乘以“loops”值,就能得到该节点实际消耗的总时间。
EXPLAIN ANALYZE显示的Total runtime包括执行器启动和关闭所需的时间,以及处理结果行所花费的时间,但不包括解析、重写和规划时间。对于SELECT查询,总运行时间通常只是比顶层计划节点报告的总时间略大一点。而对于INSERT、UPDATE和DELETE命令,总运行时间可能会大得多,因为它包含处理结果行所花费的时间。对于这些命令,顶层计划节点的时间基本上就是定位旧行和/或计算新行所花费的时间,但不包括做出这些更改所花费的时间。
值得注意的是,不应把EXPLAIN的结果外推到实际测试场景之外的情况;例如,不能假定在很小的表上得到的结果也适用于大型表。规划器的代价估计不是线性的,因此它可能会为更大或更小的表选择不同的计划。一个极端示例是:对于只占用一个磁盘页的表,无论索引是否可用,几乎总会得到顺序扫描计划。规划器认识到,无论如何处理该表都需要一次磁盘页读取,因此再额外读取页面去查看索引并没有价值。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。