Table of Contents
本章建立在Section 14.1和Section 14.2所介绍的内容之上,进一步说明规划器如何利用系统统计信息来估计查询各部分可能返回的行数。这是规划过程中的重要组成部分,为代价计算提供了大量原始材料。
本章的目的不是详细说明代码,而是概述其工作方式。这或许能让随后想要阅读代码的人更容易入门。
下面的示例使用PostgreSQL回归测试数据库中的表。另请注意,由于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(mcv_freqs))/(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的频率加总,再从 1 中减去,然后除以其他不同值的数量。这相当于假设该列中不属于任何 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的高频值信息,这里是它的直方图:
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'对前 6 项成立,对后 4 项不成立,因此在由 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几乎落在第三个直方图桶的末尾。规划器基于一些相当粗略的关于不同字符出现频率的假设,得到直方图所表示总体中小于IAAAAA的那一部分估计为 0.298387。然后我们将 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 |
在这种情况下,没有关于unique2的MCV信息,而且所有值看起来都是唯一的(n_distinct = -1),因此我们使用一种依赖两个关系的行数估计(num_rows,文中未显示,但在本例中两者都是 10000)以及列空值比例(两者都为 0)的算法:
selectivity = (1 - null_frac1) * (1 - null_frac2) / max(num_rows1, num_rows2)
= (1 - 0) * (1 - 0) / max(10000, 10000)
= 0.0001
也就是说,对每个关系都用 1 减去空值比例,再除以较大关系的行数(在非唯一情形下,这个值会被缩放)。连接预计输出的行数,计算方法是两个输入的笛卡尔积基数乘以选择率:
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中。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。