下面的示例使用 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 函数从
pg_statistic 中取出
unique1 的直方图。对于手工查询,更方便的
方式是查看较为简单的 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 的 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 几乎落在第三个
直方图桶的末尾。基于一些关于不同字符频率的粗糙假设,规划器估算出
直方图部分中小于 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)
注意,从位图索引扫描返回的行数估算只反映与索引一起使用的那个条件; 这一点很重要,因为它会影响后续堆取回(heap fetch)的代价估算。
最后我们来考察一个涉及连接的查询:
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。
操作符就是我们熟悉的 =,不过选择性函数要从
pg_operator 的 oprjoin
列获取,为 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,图中未显示,但为 "tenk")以及两列
空值比例(均为零)的算法:
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。