以下所示的示例使用 PostgreSQL 回归测试数据库中的表。同时请注意,由于 ANALYZE
在生成统计信息时使用随机抽样,因此在任何新的 ANALYZE
后,结果都会略微改变。
让我们从一个非常简单的查询开始
EXPLAIN SELECT * FROM tenk1; QUERY PLAN ------------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
计划程序如何确定 tenk1
基数在 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
。对于相等估计,直方图无用;而 最常见值 (MCVs) 列表用于确定选择性。我们来看看 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 列表中,选择性仅为最常见频次列表中对应的条目 (MCFs)
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列表中时,如何估计选择性。该方法是利用该值不在列表中的事实,结合对所有MCVs
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
的频率的了解MCVs
rows = 10000 * 0.0014559 = 15 (rounding off)
的频率加起来,再从一中减去,然后除以 其他 唯一值的个数。这等同于假设不属于任何 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)
前面带有 unique1 < 1000
的示例过于简化了 scalarltsel
真正的作用;现在我们已经看到了 MCV 使用示例,我们可以补充一些更多细节。该示例所述内容是正确的,因为由于 unique1
是唯一列,因此它没有 MCV(显然,没有哪个值比任何其他值更常见)。对于非唯一列,通常会有直方图和 MCV 列表,而且 直方图不包括由 MCV 表示的列总体部分。我们这样做是因为这样可以进行更精确的估计。在这种情况下,scalarltsel
将条件(例如,“< 1000”)直接应用到 MCV 列表的每个值,并累加条件为真的 MCV 的频率。这提供了 MCV 所在表部分内选择性的确切估计。然后,直方图以与上面相同的方式用于估计不在 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
。操作符是我们熟悉的 =
,但选择性函数从 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 |
在这种情况下,没有MCV为 unique2
提供信息,并且所有值似乎都是唯一的(n_distinct = -1),因此我们使用一种算法,该算法依赖于两个关系的行计数估计(num_rows,不显示,但为“tenk”)以及列空值部分(两者的为 0)
selectivity = (1 - null_frac1) * (1 - null_frac2) / max(num_rows1, num_rows2) = (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
执行的每个内部索引扫描获得的行数的估计数量。但这并不是事实:在考虑任何特定连接计划之前,将估计连接关系大小。如果一切都很好,那么估计连接大小的两种方法得到的答案大体相同,但由于舍入误差和其他因素,它们有时会显著发散。
对于那些有兴趣了解更多详情的人来说,在 src/backend/optimizer/util/plancat.c
中完成表的大小估计(在任何 WHERE
子句之前)。子句选择性的通用逻辑位于 src/backend/optimizer/path/clausesel.c
。操作符特定选择性功能主要位于 src/backend/utils/adt/selfuncs.c
。