↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1
历史版本PostgreSQL 8.2 已于 2011 年 12 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本。

第 54 章 规划器如何使用统计信息

本章建立在第 13.1 节和第 13.2 节所介绍的内容之上,说明规划器如何利用系统统计信息来估计查询各阶段可能返回的行数。这是规划/优化过程的重要组成部分,为代价计算提供了大量原始材料。

本章的目的不是为代码编写文档 — 那更适合在代码本身中完成,而是概述其工作方式。这或许能让随后想要阅读代码的人更容易入门。因此,我们选择的方式是分析一系列复杂度递增的示例。

下面展示的输出和算法取自版本 8.0。更早(或更晚)的版本行为可能有所不同。

54.1. 行估计示例 #

使用取自回归测试数据库的示例,让我们从一个很简单的查询开始:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

关于规划器如何确定 tenk1 的基数, 第 13.1 节 中已经介绍过;这里为了完整起见再 重复一次。行数是从 pg_class 中查得的:

SELECT reltuples, relpages FROM pg_class WHERE relname = 'tenk1';

 relpages | reltuples
----------+-----------
      345 |     10000

规划器会检查relpages估计 (这是一项开销很小的操作),如果它不正确,就可能对 reltuples进行缩放以获得行数估计。 本例中没有这样做,因此:

rows = 10000

让我们接着看一个在WHERE子句中包含范围条件的示例:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=1031 width=244)
   Filter: (unique1 < 1000)

规划器检查WHERE子句中的条件:

unique1 < 1000

并在pg_operator中查找操作符<的限制函数。它保存在oprrest列中,本例的结果为scalarltsel。scalarltsel函数从pg_statistics中取得unique1的直方图 — 我们可以通过更简单的pg_stats视图来查看它:

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='unique1';

                   histogram_bounds
------------------------------------------------------
 {1,970,1943,2958,3971,5069,6028,7007,7919,8982,9995}

接下来,计算“< 1000”在直方图中所占的比例,这就是选择率。直方图把范围划分为等频率的桶,因此只需找到目标值所在的桶,计入该桶的部分和前面所有桶的全部。值 1000 显然在第二个桶(970-1943)中。假设每个桶内部的值呈线性分布,可以按如下方式计算选择率:

selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets
            = (1 + (1000 - 970)/(1943 - 970))/10
            = 0.1031

即一个完整的桶加上第二个桶中的线性比例,再除以桶数。用选择率乘以tenk1的基数,即可计算估计行数:

rows = rel_cardinality * selectivity
     = 10000 * 0.1031
     = 1031

接下来考虑一个在WHERE子句中包含等值条件的示例:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'ATAAAA';

                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=31 width=244)
   Filter: (stringu1 = 'ATAAAA'::name)

规划器同样检查WHERE子句中的条件:

stringu1 = 'ATAAAA'

并查找=的限制函数,即eqsel。 这个例子有些不同,因为要使用最常见值 — MCV 来确定选择率。查看这些值,同时带上一些后面会有用的额外列:

SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';

null_frac         | 0
n_distinct        | 672
most_common_vals  | {FDAAAA,NHAAAA,ATAAAA,BGAAAA,EBAAAA,MOAAAA,NDAAAA,OWAAAA,BHAAAA,BJAAAA}
most_common_freqs | {0.00333333,0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.00266667,0.00266667}

选择率就是第三个MCV — 'ATAAAA' 对应的最常见 值频率(MCF):

selectivity = mcf[3]
            = 0.003

估计行数与之前一样,是这个值与tenk1基数的乘积:

rows = 10000 * 0.003
     = 30

由于一些估计后的检查,EXPLAIN显示的数字比它多 1。

现在考虑同一个查询,但使用一个不在MCV列表中的常量:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx';

                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=15 width=244)
   Filter: (stringu1 = 'xxx'::name)

这是一个完全不同的问题:当值不在MCV 列表中时,如何估计选择率。方法是利用该值不在列表中的事实,再结合所有 MCV的频率信息:

selectivity = (1 - sum(mvf))/(num_distinct - num_mcv)
            = (1 - (0.00333333 + 0.00333333 + 0.003 + 0.003 + 0.003
            + 0.003 + 0.003 + 0.003 + 0.00266667 + 0.00266667))/(672 - 10)
            = 0.001465

也就是说,把所有MCV的频率加起来,再用一减去这个 总和 — 因为该值不是其中之一 — 然后除以 其余的非重复值数量。注意,这里没有空值,所以不必 考虑它们。估计行数照常计算:

rows = 10000 * 0.001465
     = 15

现在考虑在WHERE子句中有多个条件的情形:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..495.00 rows=2 width=244)
   Filter: ((unique1 < 1000) AND (stringu1 = 'xxx'::name))

规划器假定这两个条件相互独立,因此可以将各个子句的选择率相乘:

selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx')
            = 0.1031 * 0.001465
            = 0.00015104

估计行数照常计算:

rows = 10000 * 0.00015104
     = 2

最后,我们来看一个同时包含JOIN和 WHERE子句的查询:

EXPLAIN SELECT *  FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;

                                      QUERY PLAN
-----------------------------------------------------------------------------------------
 Nested Loop  (cost=0.00..346.90 rows=51 width=488)
   ->  Index Scan using tenk1_unique1 on tenk1 t1  (cost=0.00..192.57 rows=51 width=244)
         Index Cond: (unique1 < 50)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..3.01 rows=1 width=244)
         Index Cond: ("outer".unique2 = t2.unique2)

对tenk1的限制条件 “unique1 < 50”在嵌套循环连接之前求值。 它的处理方式与前面的范围示例类似。<的限制函数 仍是scalarlteqsel,但这次值 50 位于 unique1直方图的第一个桶中:

selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets
            = (0 + (50 - 1)/(970 - 1))/10
            = 0.005057

rows        = 10000 * 0.005057
            = 51

连接的限制条件是:

t2.unique2 = t1.unique2

这是因为连接方法是嵌套循环,而tenk1位于外层循环。 操作符仍是我们熟悉的=,但限制函数是从 pg_operator的oprjoin列 获得的,它是eqjoinsel。此外,我们要同时使用 tenk2和tenk1的统计信息:

SELECT tablename, null_frac,n_distinct, most_common_vals FROM pg_stats
WHERE tablename IN ('tenk1', 'tenk2') AND attname='unique2';

tablename  | null_frac | n_distinct | most_common_vals
-----------+-----------+------------+------------------
 tenk1     |         0 |         -1 |
 tenk2     |         0 |         -1 |

这里unique2没有MCV信息, 因为所有值看起来都是唯一的,所以我们可以使用一种只依赖两个关系的 不同值数量以及它们空值比例的算法:

selectivity = (1 - null_frac1) * (1 - null_frac2) * min(1/num_distinct1, 1/num_distinct2)
            = (1 - 0) * (1 - 0) * min(1/10000, 1/1000)
            = 0.0001

也就是说,对每个关系用一减去其空值比例,再除以两个不同值数量中 较大的那个。连接可能产生的行数按嵌套循环中两个节点的笛卡尔积基数 乘以选择率计算:

rows = (outer_cardinality * inner_cardinality) * selectivity
     = (51 * 10000) * 0.0001
     = 51

如果有兴趣了解更多细节,关系中行数的估计位于 src/backend/optimizer/util/plancat.c。子句选择率的 计算逻辑位于src/backend/optimizer/path/clausesel.c。 操作符与连接限制函数的实际实现可在 src/backend/utils/adt/selfuncs.c中找到。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。