2024 年 9 月 26 日: PostgreSQL 17 发布!
支持版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持的版本: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 行,它的索引也包含 10000 行,但索引(不出所料)比表小得多。

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

大多数查询仅检索表中的一部分行,因为 WHERE 子句限制了要检查的行。因此,规划器需要估计 WHERE 子句的 选择性,即匹配 WHERE 子句中每个条件的行所占的比例。用于此任务的信息存储在 pg_statistic 系统目录中。中的条目 pg_statisticANALYZEVACUUM 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 列的前十个最常见值。)

使用 ANALYZEpg_statistic 中存储的信息量,特别是每列 most_common_valshistogram_bounds 数组中的最大条目数,可以通过 ALTER TABLE SET STATISTICS 命令在每列的基础上设置,或通过设置 default_statistics_target 配置变量在全局范围内设置。默认限制目前为 100 个条目。提高限制可能会允许更准确的规划器估计,特别是对于数据分布不规则的列,但代价是在 pg_statistic 中消耗更多空间,以及计算估计值的时间略微增加。相反,对于数据分布简单的列,较低的限制可能就足够了。

有关规划器使用统计信息的更多详细信息,请参见 第 68 章.

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和规划周期将被浪费。

提交更正

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