2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2

14.2. 查询规划器使用的统计信息 #

14.2.1. 单列统计信息 #

正如我们在上一节看到的,查询规划器需要估算查询返回的行数,以便做出最佳的查询计划选择。本节将简要介绍系统用于这些估算的统计信息。

统计信息的一个组成部分是每个表和索引的总条目数,以及每个表和索引占用的磁盘块数。这些信息保存在 pg_class 表的 reltuplesrelpages 列中。我们可以通过类似这样的查询来查看它

SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'tenk1%';

       relname        | relkind | reltuples | relpages
----------------------+---------+-----------+----------
 tenk1                | r       |     10000 |      345
 tenk1_hundred        | i       |     10000 |       11
 tenk1_thous_tenthous | i       |     10000 |       30
 tenk1_unique1        | i       |     10000 |       30
 tenk1_unique2        | i       |     10000 |       30
(5 rows)

在这里我们可以看到 tenk1 包含 10000 行,它的索引也是如此,但这些索引(毫不奇怪)比表本身小得多。

出于效率原因,reltuplesrelpages 不是即时更新的,因此它们通常包含略微过时的值。它们由 VACUUMANALYZE 和一些 DDL 命令(如 CREATE INDEX)更新。一个不扫描整个表的 VACUUMANALYZE 操作(这很常见)将基于它扫描的部分表来增量更新 reltuples 计数,从而得到一个近似值。无论如何,规划器都会将它在 pg_class 中找到的值缩放到匹配当前的物理表大小,从而得到一个更接近的近似值。

大多数查询只会检索表中一小部分行,因为 WHERE 子句限制了要检查的行。因此,规划器需要估算 WHERE 子句的选择性,即每一项条件匹配的行所占的比例。用于此任务的信息存储在 pg_statistic 系统目录中。 pg_statistic 中的条目由 ANALYZEVACUUM ANALYZE 命令更新,并且即使在刚更新时也是近似值。

与直接查看 pg_statistic 相比,在手动检查统计信息时,最好查看其视图 pg_statspg_stats 的设计更易于阅读。此外,pg_stats 对所有人可读,而 pg_statistic 仅对超级用户可读。(这可以防止没有权限的用户从统计信息中了解其他人的表内容。 pg_stats 视图被限制为只显示当前用户可以读取的表的行。)例如,我们可以这样做

SELECT attname, inherited, n_distinct,
       array_to_string(most_common_vals, E'\n') as most_common_vals
FROM pg_stats
WHERE tablename = 'road';

 attname | inherited | n_distinct |          most_common_vals
---------+-----------+------------+------------------------------------
 name    | f         | -0.5681108 | I- 580                        Ramp+
         |           |            | I- 880                        Ramp+
         |           |            | Sp Railroad                       +
         |           |            | I- 580                            +
         |           |            | I- 680                        Ramp+
         |           |            | I- 80                         Ramp+
         |           |            | 14th                          St  +
         |           |            | I- 880                            +
         |           |            | Mac Arthur                    Blvd+
         |           |            | Mission                       Blvd+
...
 name    | t         |    -0.5125 | I- 580                        Ramp+
         |           |            | I- 880                        Ramp+
         |           |            | I- 580                            +
         |           |            | I- 680                        Ramp+
         |           |            | I- 80                         Ramp+
         |           |            | Sp Railroad                       +
         |           |            | I- 880                            +
         |           |            | State Hwy 13                  Ramp+
         |           |            | I- 80                             +
         |           |            | State Hwy 24                  Ramp+
...
 thepath | f         |          0 |
 thepath | t         |          0 |
(4 rows)

请注意,对于同一列会显示两行,一行对应以 road 表开头的完整继承层级(inherited=t),另一行仅包含 road 表本身(inherited=f)。(为简洁起见,我们仅显示了 name 列的前十个最常见值。)

ANALYZE 存储在 pg_statistic 中的信息量,特别是每个列的 most_common_valshistogram_bounds 数组的最大条目数,可以通过 ALTER TABLE SET STATISTICS 命令在列级别上设置,或者通过设置 default_statistics_target 配置变量来全局设置。默认限制目前是 100 个条目。提高此限制可能有助于提高规划器估算的准确性,尤其对于数据分布不规律的列,但代价是 pg_statistic 占用更多空间,计算估算值也需要更多时间。反之,对于数据分布简单的列,较低的限制可能就足够了。

有关规划器如何使用统计信息的更多详细信息,请参阅 第 69 章

14.2.2. 扩展统计信息 #

由于查询子句中使用的多个列之间存在相关性,经常会导致查询运行缓慢并生成糟糕的执行计划。规划器通常假设多个条件是相互独立的,当列值相关时,这种假设就不成立了。常规统计信息由于其单列的性质,无法捕捉到跨列相关性的任何信息。然而,PostgreSQL 能够计算多变量统计信息,这可以捕捉到这种信息。

由于可能的列组合数量非常庞大,自动计算多变量统计信息是不切实际的。取而代之的是,可以创建扩展统计信息对象(通常简称为统计信息对象),以指示服务器收集跨感兴趣的列集进行统计。

统计信息对象使用 CREATE STATISTICS 命令创建。创建此类对象只是创建了一个目录条目,表示对该统计信息的兴趣。实际的数据收集由 ANALYZE 执行(无论是手动命令还是后台自动分析)。收集到的值可以在 pg_statistic_ext_data 目录中查看。

ANALYZE 根据它为收集常规单列统计信息所采样的表行样本来计算扩展统计信息。由于通过提高表或其任何列的统计信息目标(如上一节所述)来增加采样大小,因此较大的统计信息目标通常会导致更准确的扩展统计信息,以及计算它们所需的时间更长。

以下子节将描述目前支持的扩展统计信息类型。

14.2.2.1. 函数依赖 #

扩展统计信息中最简单的类型是跟踪函数依赖,这是数据库范式定义中使用的概念。我们说列 b 函数依赖于列 a,如果知道 a 的值足以确定 b 的值,也就是说,没有两行具有相同的 a 值但不同的 b 值。在完全范式化的数据库中,函数依赖应该只存在于主键和超键上。然而,在实践中,许多数据集由于各种原因并未完全范式化;出于性能原因的有意反范式化是一个常见例子。即使在完全范式化的数据库中,某些列之间也可能存在部分相关性,这可以表示为部分函数依赖。

函数依赖的存在直接影响某些查询估算的准确性。如果查询同时包含独立列和依赖列上的条件,则对依赖列的条件不会进一步减小结果集的大小;但如果没有函数依赖的知识,查询规划器将假定这些条件是独立的,从而低估结果集的大小。

为了告知规划器关于函数依赖的信息,ANALYZE 可以收集跨列依赖性的测量数据。评估所有列集之间的依赖程度成本过高,因此数据收集仅限于出现在具有 dependencies 选项定义的统计信息对象中的列组。建议仅为强相关的列组创建 dependencies 统计信息,以避免在 ANALYZE 和后续查询规划中产生不必要的开销。

以下是收集函数依赖统计信息的示例

CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes;

ANALYZE zipcodes;

SELECT stxname, stxkeys, stxddependencies
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts';
 stxname | stxkeys |             stxddependencies
---------+---------+------------------------------------------
 stts    | 1 5     | {"1 => 5": 1.000000, "5 => 1": 0.423130}
(1 row)

在这里可以看到,第 1 列(邮政编码)完全确定第 5 列(城市),因此系数为 1.0;而城市仅在约 42% 的情况下确定邮政编码,这意味着许多城市(58%)由不止一个邮政编码表示。

在计算涉及函数依赖列的查询的选择性时,规划器会使用依赖性系数调整每个条件的估计选择性,以避免产生低估。

14.2.2.1.1. 函数依赖的局限性 #

函数依赖目前仅在考虑将列与常量值进行比较的简单相等条件以及带有常量值的 IN 子句时应用。它们不用于改进比较两个列或将列与表达式进行比较的相等条件,也不用于范围子句、LIKE 或任何其他类型的条件。

在进行函数依赖估算时,规划器假定涉及列上的条件是兼容的,因此是冗余的。如果它们不兼容,正确的估算将是零行,但这种情况不会被考虑。例如,对于如下查询:

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '94105';

规划器将忽略 city 子句,因为它不会改变选择性,这是正确的。然而,它会对以下情况做出相同的假设:

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '90210';

即使实际上没有行满足此查询。然而,函数依赖统计信息不足以得出此结论。

在许多实际情况下,这种假设通常是满足的;例如,应用程序中可能有一个 GUI,只允许选择兼容的城市和邮政编码值来用于查询。但如果情况并非如此,函数依赖可能就不是一个可行的选项。

14.2.2.2. 多变量 N-Distinct 计数 #

单列统计信息存储每列的独立值数量。当组合多个列时(例如,对于 GROUP BY a, b),对独立值数量的估算经常出错,当规划器仅拥有单列统计数据时,这会导致其选择糟糕的计划。

为了改进此类估算,ANALYZE 可以为列组收集 n-distinct 统计信息。与之前一样,为每个可能的列组合执行此操作是不切实际的,因此数据仅为出现在具有 ndistinct 选项定义的统计信息对象中的列组收集。将为列集中每个可能的两个或多个列的组合收集数据。

继续前面的示例,表中邮政编码的 n-distinct 计数可能如下所示

CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes;

ANALYZE zipcodes;

SELECT stxkeys AS k, stxdndistinct AS nd
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts2';
-[ RECORD 1 ]------------------------------------------------------​--
k  | 1 2 5
nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178}
(1 row)

这表明有三个列组合拥有 33178 个不同的值:邮政编码和州;邮政编码和城市;以及邮政编码、城市和州(它们都相等的事实是因为邮政编码本身在这个表中是唯一的)。另一方面,城市和州的组合只有 27435 个不同的值。

建议仅为实际用于分组且对分组数量的错误估算导致了糟糕的计划的列组合创建 ndistinct 统计信息对象。否则,ANALYZE 的周期就会被浪费。

14.2.2.3. 多变量 MCV 列表 #

为每列存储的另一种统计信息是最常见值列表。这允许对单个列进行非常准确的估算,但可能导致对具有多个列条件的查询产生严重的错误估算。

为了改进此类估算,ANALYZE 可以为列组合收集 MCV 列表。与函数依赖和 n-distinct 系数类似,为每个可能的列组合执行此操作是不切实际的。在此情况下更是如此,因为 MCV 列表(与函数依赖和 n-distinct 系数不同)确实存储了常见的列值。因此,数据仅为出现在具有 mcv 选项定义的统计信息对象中的列组收集。

继续前面的示例,邮政编码表的 MCV 列表可能如下所示(与更简单的统计信息类型不同,需要一个函数来检查 MCV 内容)

CREATE STATISTICS stts3 (mcv) ON city, state FROM zipcodes;

ANALYZE zipcodes;

SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid),
                pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts3';

 index |         values         | nulls | frequency | base_frequency
-------+------------------------+-------+-----------+----------------
     0 | {Washington, DC}       | {f,f} |  0.003467 |        2.7e-05
     1 | {Apo, AE}              | {f,f} |  0.003067 |        1.9e-05
     2 | {Houston, TX}          | {f,f} |  0.002167 |       0.000133
     3 | {El Paso, TX}          | {f,f} |     0.002 |       0.000113
     4 | {New York, NY}         | {f,f} |  0.001967 |       0.000114
     5 | {Atlanta, GA}          | {f,f} |  0.001633 |        3.3e-05
     6 | {Sacramento, CA}       | {f,f} |  0.001433 |        7.8e-05
     7 | {Miami, FL}            | {f,f} |    0.0014 |          6e-05
     8 | {Dallas, TX}           | {f,f} |  0.001367 |        8.8e-05
     9 | {Chicago, IL}          | {f,f} |  0.001333 |        5.1e-05
   ...
(99 rows)

这表明最常见的城市和州组合是华盛顿特区,实际频率(在样本中)约为 0.35%。组合的基础频率(从简单的单列频率计算得出)仅为 0.0027%,导致低估了两个数量级。

建议创建MCV统计信息对象仅用于实际一起出现在条件中的列组合,并且对分组数量的错误估算导致了糟糕的计划。否则,ANALYZE 和规划周期就会被浪费。

提交更正

如果您在文档中发现任何不正确、与您的实际经验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。