受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

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列前十个最常见值。)

ANALYZEpg_statistic中存储的信息量,特别是每列most_common_vals中的最大项数和histogram_bounds数组的大小,可以使用ALTER TABLE SET STATISTICS命令按列设置,也可以通过设置配置变量default_statistics_target进行全局设置。目前默认上限是 100 项。提高这一上限可能让规划器做出更准确的估计,尤其是对于数据分布不规则的列;代价则是pg_statistic占用更多空间,并且计算估计值所需时间也会略有增加。相反,对于数据分布较简单的列,较低的上限可能已经足够。

更多规划器对统计信息的使用可参阅Chapter 65

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';

不过,函数依赖统计信息无法提供足够的信息来排除这种情况。

在很多实际场景中,这种假设通常是成立的。例如,应用程序中可能存在一个图形界面,只允许选择彼此兼容的城市和邮编值用于查询。但如果不是这样,函数依赖可能就不是一个可行的选项。

14.2.2.2. 多元可区分值计数 #

单列统计信息存储每一列中可区分值的数量。在组合多个列(例如GROUP BY a, b)时,如果规划器只有单列统计数据,则对可区分值数量的估计常常会错误,导致选择不好的计划。

为了改进这种估计,ANALYZE可以为列组收集可区分值统计信息。和以前一样,为每一种可能的列组合做这件事情是不切实际的,因此只会为一起出现在一个统计信息对象(用ndistinct选项定义)中的列组收集数据。将会为列组中列出的列的每一种可能的组合都收集数据。

继续前面的示例,ZIP 代码表中的可区分值计数可能如下所示:

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 个可区分值:ZIP 代码和州、ZIP 代码和城市,以及 ZIP 代码、城市和州(考虑到在这个表中仅靠 ZIP 代码就已唯一,这三者相等本来就是预期中的)。另一方面,城市和州的组合只有 27435 个可区分值。

建议只对实际用于分组的列组合以及分组数错误估计导致了糟糕计划的列组合创建ndistinct统计信息对象。否则,ANALYZE循环只会被浪费。

14.2.2.3. 多元 MCV 列表 #

另一类按列存储的统计信息是最常见值列表。这允许对单列做出非常准确的估计,但对于同时涉及多个列条件的查询,仍可能产生显著误差。

为了改进这类估计,ANALYZE可以收集列组合上的 MCV 列表。 与函数依赖和 n-distinct 系数类似,对每一种可能的列分组都这样做并不现实。 对 MCV 列表而言尤其如此,因为与函数依赖和 n-distinct 系数不同,它确实要存储常见的列值本身。 因此,只会为那些共同出现在使用mcv选项定义的统计信息对象中的列组收集数据。

继续前面的示例,ZIP 代码表上的 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 行)

这表明城市和州的最常见组合是哥伦比亚特区的 Washington,其实际频率(在样本中)约为 0.35%。 该组合的基础频率,也就是根据简单的逐列频率计算出来的频率,仅为 0.0027%,从而低估了两个数量级。

建议只为那些在条件中确实会一起使用,而且错误估计会导致糟糕执行计划的列组合创建MCV统计对象。 否则,只会浪费ANALYZE和规划时间。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。