本章建立在Section 14.1和Section 14.2所介绍的内容之上,进一步说明规划器如何利用系统统计信息来估计查询各部分可能返回的行数。这是规划过程中的重要组成部分,为代价计算提供了大量原始材料。
本章的目的不是详细说明代码,而是概述其工作方式。这或许能让随后想要阅读代码的人更容易入门。
下面的示例使用 PostgreSQL 回归测试数据库中的表。所示输出取自版本 8.3,更早(或更晚)的版本行为可能有所不同。另外,由于 ANALYZE 在生成统计信息时使用随机采样,每次重新执行 ANALYZE 后,结果都会略有变化。
让我们从一个很简单的查询开始:
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
关于规划器如何确定tenk1的基数,Section 14.2中已经介绍过;这里为了完整起见再重复一次。页数和行数是从pg_class中查得的:
SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
relpages | reltuples
----------+-----------
358 | 10000
这些数字反映的是该表最近一次VACUUM或ANALYZE时的情况。随后,规划器会取得该表当前实际的页数(这是一项开销很小的操作,无需扫描全表)。如果该值与relpages不同,就会相应地缩放reltuples,从而得到当前的行数估计。在上面的示例中,relpages的值是最新的,因此行数估计与reltuples相同。
接着看一个包含范围条件的示例,其条件位于WHERE子句中:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;
QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=24.06..394.64 rows=1007 width=244)
Recheck Cond: (unique1 < 1000)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..23.80 rows=1007 width=0)
Index Cond: (unique1 < 1000)
规划器检查WHERE子句中的条件,并查找操作符<的选择率函数,所在的系统目录是pg_operator。此函数保存在oprrest列中,本例的条目为scalarltsel。scalarltsel函数取得unique1的直方图,来源为pg_statistic。手动查询时,查看更简单的pg_stats视图更方便:
SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='unique1';
histogram_bounds
------------------------------------------------------
{0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}
接下来,计算“< 1000”在直方图中所占的比例,这就是选择率。直方图把范围划分为等频率的桶,因此只需找到目标值所在的桶,计入该桶的部分和前面所有桶的全部。值 1000 显然在第二个桶(993–1997)中。假设每个桶内部的值呈线性分布,可以按如下方式计算选择率:
selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets
= (1 + (1000 - 993)/(1997 - 993))/10
= 0.100697
即一个完整的桶加上第二个桶中的线性比例,再除以桶数。用选择率乘以表的基数,即可计算估计行数。这里所用的表为tenk1:
rows = rel_cardinality * selectivity
= 10000 * 0.100697
= 1007 (rounding off)
接下来,考虑一个包含等值条件的示例,其条件位于WHERE子句中:
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA';
QUERY PLAN
----------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..483.00 rows=30 width=244)
Filter: (stringu1 = 'CRAAAA'::name)
规划器同样会检查WHERE子句中的条件,查找操作符=的选择率函数,即eqsel。对于等值估计,直方图没有用;应该使用高频值 (MCV)列表来确定选择率。查看 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 | 676
most_common_vals | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,JOAAAA,MCAAAA,NAAAAA,WGAAAA}
most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003}
由于CRAAAA出现在 MCV 列表中,选择率就是最常见值频率(MCF)列表中的对应条目:
selectivity = mcf[3]
= 0.003
与之前一样,估计行数就是这个值与表的基数的乘积。这里所用的表为tenk1:
rows = 10000 * 0.003
= 30
现在考虑同一个查询,但使用一个不在MCV列表中的常量:
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx';
QUERY PLAN
----------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..483.00 rows=15 width=244)
Filter: (stringu1 = 'xxx'::name)
这是一个完全不同的问题:当值不在MCV列表中时,如何估计选择率。方法是利用该值不在列表中的事实,再结合所有MCV的频率信息:
selectivity = (1 - sum(mvf))/(num_distinct - num_mcv)
= (1 - (0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 +
0.003 + 0.003 + 0.003 + 0.003))/(676 - 10)
= 0.0014559
也就是说,把所有MCV的频率加起来,用一减去这个总和,再除以其他非重复值的数量。这相当于假设该列中不属于任何 MCV 的那部分值,均匀分布在其他所有非重复值上。注意,这里没有空值,所以不必考虑它们(否则还应从分子中减去空值比例)。然后照常计算估计行数:
rows = 10000 * 0.0014559
= 15 (rounding off)
前面条件为unique1 < 1000的示例,过度简化了scalarltsel的实际行为。现在已经看过 MCV 的使用示例,可以补充一些细节。前面的示例就其涉及的范围而言是正确的,因为unique1是唯一列,所以没有 MCV(显然,没有哪个值会比其他值更常见)。对于非唯一列,通常既有直方图,也有 MCV 列表,而且直方图不包括该列总体中由 MCV 表示的那一部分。这样处理可以使估计更准确。在这种情况下,scalarltsel直接将条件(例如“< 1000”)应用于 MCV 列表中的每个值,并将满足条件的 MCV 的频率相加。这可以精确估计表中 MCV 所代表部分的选择率。然后按前面所述的方法使用直方图,估计表中非 MCV 部分的选择率,再将两个值结合起来,估计整体选择率。例如,考虑以下查询:
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 < 'IAAAAA';
QUERY PLAN
------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..483.00 rows=3077 width=244)
Filter: (stringu1 < 'IAAAAA'::name)
我们已经看过stringu1的 MCV 信息,下面是它的直方图:
SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';
histogram_bounds
--------------------------------------------------------------------------------
{AAAAAA,CQAAAA,FRAAAA,IBAAAA,KRAAAA,NFAAAA,PSAAAA,SGAAAA,VAAAAA,XLAAAA,ZZAAAA}
检查 MCV 列表可以发现,条件stringu1 < 'IAAAAA'被前六个条目满足,而后四个条目不满足,因此总体中 MCV 部分的选择率为:
selectivity = sum(relevant mvfs)
= 0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003
= 0.01833333
将所有 MCF 相加,还可以得知 MCV 所代表的部分占总体的比例为 0.03033333,因此直方图所代表的部分占比为 0.96966667(这里同样没有空值,否则还需要将其排除)。可以看到,值IAAAAA接近第三个直方图桶的末尾。规划器对不同字符的频率作出一些粗略假设,估计直方图所代表的总体中有 0.298387 的部分小于IAAAAA。然后将 MCV 和非 MCV 两部分的估计结合起来:
selectivity = mcv_selectivity + histogram_selectivity * histogram_fraction
= 0.01833333 + 0.298387 * 0.96966667
= 0.307669
rows = 10000 * 0.307669
= 3077 (rounding off)
在这个特定的示例中,MCV 列表带来的修正相当小,因为该列的分布实际上相当均匀(统计信息显示这些特定值比其他值更常见,主要是采样误差造成的)。更常见的情况是,某些值明显比其他值更常见;这时,这个复杂过程能够有效提高准确性,因为最常见值的选择率是精确求得的。
现在考虑另一种情形,其中多个条件出现在WHERE子句中:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';
QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=23.80..396.91 rows=1 width=244)
Recheck Cond: (unique1 < 1000)
Filter: (stringu1 = 'xxx'::name)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..23.80 rows=1007 width=0)
Index Cond: (unique1 < 1000)
规划器假定这两个条件相互独立,因此可以将各个子句的选择率相乘:
selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx')
= 0.100697 * 0.0014559
= 0.0001466
rows = 10000 * 0.0001466
= 1 (rounding off)
注意,位图索引扫描的估计返回行数只反映用于索引的条件。这一点很重要,因为它会影响后续堆访问的代价估计。
最后,来看一个涉及连接的查询:
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;
QUERY PLAN
--------------------------------------------------------------------------------------
Nested Loop (cost=4.64..456.23 rows=50 width=488)
-> Bitmap Heap Scan on tenk1 t1 (cost=4.64..142.17 rows=50 width=244)
Recheck Cond: (unique1 < 50)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.63 rows=50 width=0)
Index Cond: (unique1 < 50)
-> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.00..6.27 rows=1 width=244)
Index Cond: (unique2 = t1.unique2)
对于tenk1, unique1 < 50这一限制会在嵌套循环连接之前求值。处理方法与之前的范围查询示例类似。这次,值 50 落在unique1直方图的第一个桶中:
selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets
= (0 + (50 - 0)/(993 - 0))/10
= 0.005035
rows = 10000 * 0.005035
= 50 (rounding off)
连接的限制条件是t2.unique2 = t1.unique2。操作符就是熟悉的=,不过选择率函数来自oprjoin列,该列属于pg_operator,本例中为eqjoinsel。 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 |
本例中,没有MCV信息可用于unique2,因为所有值看起来都是唯一的。因此使用的算法仅依赖于两个关系的非重复值数量及其空值比例:
selectivity = (1 - null_frac1) * (1 - null_frac2) * min(1/num_distinct1, 1/num_distinct2)
= (1 - 0) * (1 - 0) / max(10000, 10000)
= 0.0001
也就是说,对两个关系分别用一减去空值比例,再除以两者非重复值数量中的较大者。预计连接输出的行数,通过将两个输入的笛卡尔积基数乘以选择率来计算:
rows = (outer_cardinality * inner_cardinality) * selectivity
= (50 * 10000) * 0.0001
= 50
如果这两列存在 MCV 列表,eqjoinsel就会通过直接比较 MCV 列表,确定由 MCV 表示的那部分列值总体中的连接选择率。剩余部分总体的估计则沿用这里展示的相同方法。
请注意,我们把inner_cardinality写成 10000,也就是tenk2未经修改的大小。单看EXPLAIN输出,似乎连接行数估计来自 50 * 1,也就是外表行数乘以对tenk2执行每次内表索引扫描得到的估计行数。但事实并非如此:连接关系的大小是在考虑任何具体连接计划之前估计的。如果一切正常,这两种连接大小估算方式会得到大致相同的答案,但由于舍入误差和其他因素,它们有时会出现明显偏差。
如果想了解更多细节,表大小(在任何WHERE子句之前)的估计是在src/backend/optimizer/util/plancat.c中完成的。子句选择率的一般逻辑位于src/backend/optimizer/path/clausesel.c。按操作符区分的选择率函数大多位于src/backend/utils/adt/selfuncs.c中。