pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
查询性能会受到许多因素的影响。其中一些可以由用户操纵,而另一些 则是系统底层设计的根本所在。
一些性能问题(例如索引创建和批量数据装载)在别处讨论。本章将 讨论EXPLAIN命令,并展示查询的细节如何影响 查询计划,从而影响整体性能。
EXPLAIN由 Tom Lane 撰写,摘自 2000-03-27 的电子邮件。
计划阅读是一门值得写一篇教程的艺术,而我还没来得及写。下面是 一些快速而粗糙的解释。
目前 EXPLAIN 给出的数字包括:
估计的启动代价(在输出扫描可以开始之前花费的时间, 例如在 SORT 节点中执行排序的时间)。
估计的总代价(如果检索所有元组——实际可能不会, 例如 LIMIT 会在付出总代价之前停止)。
该计划节点估计输出的行数。
该计划节点估计输出的行的平均宽度(字节)。
代价以磁盘页获取次数为单位度量。(CPU 工作量估计用一些相当 任意的主观因子换算成磁盘页单位。如果你想试验这些因子,请参阅 SET参考页。)重要的是要注意,上层节点的 代价包括其所有子节点的代价。同样重要的是要认识到,代价只反映 规划器/优化器关心的东西。特别地,代价不考虑把结果元组传输到 前端所花的时间——这可能是实际耗时的一个相当主要的因素,但 规划器忽略它,因为它无法通过改变计划来改变它。(我们相信 每个正确的计划都会输出相同的元组集。)
输出行数有点棘手,因为它不是查询处理/扫描的 行数——它通常更少,反映在该节点上应用的所有 WHERE 子句 约束的估计选择率。
平均宽度是相当虚假的,因为系统实际上并不知道变长列的平均 长度。我在考虑将来改进这一点,但可能不值得费这个麻烦,因为 宽度的用途不多。
下面是一些例子(使用经过 vacuum analyze 的回归测试数据库, 以及接近 7.0 的源代码):
regression=# explain select * from tenk1;
NOTICE: 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)。
现在让我们修改查询,加一个限定子句:
regression=# explain select * from tenk1 where unique1 < 1000;
NOTICE: QUERY PLAN:
Seq Scan on tenk1 (cost=0.00..358.00 rows=1000 width=148)
由于 WHERE 子句,输出行数的估计下降了。(估计异常准确只是因为 tenk1 是一个特别简单的例子——unique1 列有 10000 个从 0 到 9999 的不同值,所以估计器在列的最小值和最大值之间做的线性 插值正好命中。)然而,扫描仍然必须访问全部 10000 行,所以 代价没有降低;实际上它还略微上升了,以反映检查 WHERE 条件 额外花费的 CPU 时间。
再修改查询,进一步收紧限定:
regression=# explain select * from tenk1 where unique1 < 100;
NOTICE: QUERY PLAN:
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..89.35 rows=100 width=148)
你会看到,如果我们让 WHERE 条件足够有选择性,规划器最终会 判定索引扫描比顺序扫描便宜。由于索引,这个计划只需访问 100 个元组,所以尽管每次单独的获取很昂贵,它仍然胜出。
再给限定加一个条件:
regression=# explain select * from tenk1 where unique1 < 100 and
regression-# stringu1 = 'xxx';
NOTICE: QUERY PLAN:
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..89.60 rows=1 width=148)
添加的子句 "stringu1 = 'xxx'" 降低了输出行数的估计,但没有 降低代价,因为我们仍然要访问同一批元组。
让我们试着用一直在讨论的字段连接两个表:
regression=# explain select * from tenk1 t1, tenk2 t2 where t1.unique1 < 100
regression-# and t1.unique2 = t2.unique2;
NOTICE: QUERY PLAN:
Nested Loop (cost=0.00..144.07 rows=100 width=296)
-> Index Scan using tenk1_unique1 on tenk1 t1
(cost=0.00..89.35 rows=100 width=148)
-> Index Scan using tenk2_unique2 on tenk2 t2
(cost=0.00..0.53 rows=1 width=148)
在这个嵌套循环连接中,外层扫描与倒数第二个例子中的索引扫描 相同,所以其代价和行数也相同,因为我们在该节点上应用的是 "unique1 < 100" WHERE 子句。"t1.unique2 = t2.unique2" 子句 此时还不相关,所以它不影响外层扫描的行数。对于内层扫描, 当前外层扫描元组的 unique2 值被代入内层索引扫描,产生一个 形如 "t2.unique2 = 常量" 的索引 限定。所以我们得到的内层扫描计划和代价,与例如 "explain select * from tenk2 where unique2 = 42" 得到的相同。循环节点 的代价随后在外层扫描代价的基础上设定,加上每个外层元组一次 的内层扫描重复(这里是 100 * 0.53),再加上一点用于连接处理 的 CPU 时间。
在这个例子中,循环的输出行数是两个扫描行数的乘积,但一般 并非如此,因为一般你可能会有同时提到两个关系的 WHERE 子句, 它们只能在连接点上应用,而不能应用于任一输入扫描。例如, 如果我们加上 "WHERE ... AND t1.hundred < t2.hundred", 那会降低连接节点的输出行数,但不改变任一输入扫描。
我们可以通过强制规划器无视它认为获胜的策略来查看不同的计划 (一个相当粗糙的工具,但我们目前只有这个):
regression=# set enable_nestloop = off;
SET VARIABLE
regression=# explain select * from tenk1 t1, tenk2 t2 where t1.unique1 < 100
regression-# and t1.unique2 = t2.unique2;
NOTICE: QUERY PLAN:
Hash Join (cost=89.60..574.10 rows=100 width=296)
-> Seq Scan on tenk2 t2
(cost=0.00..333.00 rows=10000 width=148)
-> Hash (cost=89.35..89.35 rows=100 width=148)
-> Index Scan using tenk1_unique1 on tenk1 t1
(cost=0.00..89.35 rows=100 width=148)
这个计划提议用同样的老索引扫描提取 tenk1 的 100 个感兴趣的 行,把它们存入一个内存散列表,然后对 tenk2 做顺序扫描,在每个 tenk2 元组上探查散列表以寻找 "t1.unique2 = t2.unique2" 的 可能匹配。读取 tenk1 并建立散列表的代价完全是散列连接的启动 代价,因为在我们能开始读取 tenk2 之前不会得到任何元组。连接 的总时间估计还包括探查散列表 10000 次的相当可观的 CPU 时间 费用。但注意,我们没有收取 10000 次 89.35;在这种计划类型中 散列表的建立只做一次。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。