支持的版本: 当前 (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 子句的选择性,即与 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 中占用更多空间,并且计算估计值的时间稍长。相反,对于数据分布简单的列,较低的限制可能就足够了。

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

14.2.2. 扩展统计信息 #

通常会看到由于查询子句中使用的多个列相关联而导致运行不良执行计划的慢查询。规划器通常假设多个条件彼此独立,当列值相关时,此假设不成立。常规统计信息由于其每个单独列的性质,无法捕获有关跨列相关的任何知识。但是,PostgreSQL 具有计算多元统计信息的能力,可以捕获此类信息。

由于可能的列组合数量非常庞大,因此自动计算多元统计信息是不切实际的。相反,可以创建扩展统计信息对象,更常见的称呼是统计信息对象,以指示服务器获取跨感兴趣列集的统计信息。

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

ANALYZE 基于它为计算常规单列统计信息而使用的相同表行样本来计算扩展统计信息。由于通过提高表或其任何列的统计目标来增加样本大小(如上一节所述),因此,更大的统计目标通常会产生更准确的扩展统计信息,以及花费更多的时间来计算它们。

以下小节描述了当前支持的扩展统计信息的种类。

14.2.2.1. 函数依赖 #

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

函数依赖关系的存在直接影响某些查询中估计值的准确性。 如果查询包含对独立列和依赖列的条件,则对依赖列的条件不会进一步减少结果大小; 但是,如果不知道函数依赖关系,查询规划器将假设条件是独立的,从而导致低估结果大小。

为了告知规划器函数依赖关系,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-唯一值计数 #

单列统计信息存储每列中唯一值的数量。当计划器只有单列统计数据时,组合多个列(例如,对于 GROUP BY a, b)时,对唯一值数量的估计经常是错误的,导致它选择错误的计划。

为了改进此类估计,ANALYZE 可以收集列组的 n-唯一值统计信息。与之前一样,对每个可能的列分组都这样做是不切实际的,因此仅为那些在使用 ndistinct 选项定义的统计信息对象中一起出现的列组收集数据。将为列出的一组列中两个或多个列的每个可能的组合收集数据。

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

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-唯一值系数类似,对每个可能的列分组都这样做是不切实际的。在这种情况下更是如此,因为 MCV 列表(与函数依赖和 n-唯一值系数不同)确实存储了常见的列值。因此,仅为那些在使用 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)

这表明最常见的城市和州组合是 DC 中的 Washington,实际频率(在样本中)约为 0.35%。该组合的基本频率(根据简单的每列频率计算)仅为 0.0027%,导致低估了两个数量级。

建议仅在以下情况下创建MCV统计信息对象,用于实际一起使用条件的列组合,并且对于这些组合,对组数的错误估计会导致错误的计划。否则,ANALYZE 和规划周期只是浪费。

提交更正

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