2024 年 9 月 26 日: PostgreSQL 17 已发布!
支持版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:开发
不支持的版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2

68.1. 行估计示例 #

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

这些数字是自表上最后一次 VACUUMANALYZE 起计的当前数字。随后,规划器获取表中当前页面的实际数量(这是一种廉价的操作,不需要表扫描)。如果该数字不同于 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。函数 scalarltselpg_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清单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

sMCV这意味着,将所有频率相加用于

rows = 10000 * 0.0014559
     = 15  (rounding off)

s 并将它们减去 1,然后根据 其他 独立值的数字划分。这等于假设不在任意 MCV 中的列分数均匀分布在所有其他独立值中。注意没有 Null 值,因此不必担心它们(否则我们将从分子中减去 Null 分数)。然后按平常方式计算估计行数

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 直接对 MCV 列表的每个值使用条件(例如,< 1000),并将满足条件的 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}

我们已经看到 stringu1 的 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 列表的更正十分微小,因为列分布实际上非常平坦(显示这些特定值比其他值更常见的统计数据主要是由于抽样误差)。在更典型的案例中,某些值比其他值明显更常见,这个复杂的过程在准确性方面产生了有用的提高,因为最常见值的 selectivity 恰好可以找到。

现在让我们考虑一个在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 = 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。该运算符是我们熟悉的=,但是 selectivity 函数是从pg_operatoroprjoin 列获得,并且是eqjoinseleqjoinsel 查找tenk2tenk1 的统计信息

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

也就是说,对于每个关系,其空值分数减一,然后除以较大关系的行计数(此值在非唯一情况下确实会按比例进行缩放)。连接可能发出的行数计算为两个输入项笛卡尔积的基数乘以 selectivity

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 中找到。

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符或需要进一步澄清,请使用 此表单 报告文档问题。