PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

69.1. 行数估算示例 #

下面的示例使用 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。