正如我们在上一节中看到的,查询规划器需要对查询检索的行数进行估计,以便对查询计划做出好的选择。本节将快速浏览系统用于这些估计的统计信息。
统计信息的一个组成部分是每个表和索引中的条目总数,以及每个表和索引所占据的磁盘块数。此信息保存在表 pg_class
中,位于列 reltuples
和 relpages
中。我们可以使用类似于以下查询来查看它
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 行,但索引(不出所料)比表小很多。
出于效率原因,reltuples
和 relpages
不会即时更新,因此它们通常包含有些过期的值。它们可以被 VACUUM
、ANALYZE
以及另一些 DDL 命令,例如 CREATE INDEX
更新。如果一个 VACUUM
或 ANALYZE
操作没有扫描整个表(这种情况很常见),它将根据扫描过的部分表的内容逐渐更新 reltuples
计数,从而得到一个近似值。无论如何,规划器都会调整它在 pg_class
中找到的值以匹配当前的物理表大小,从而得到一个更加接近的近似值。
大多数查询只提取表中的一部分行,这是因为 WHERE
子句限制了要检查的行。因此,规划器需要对 WHERE
子句的选择性进行估计,即满足 WHERE
子句中每个条件的行占全部行的比例。用于此任务的信息存储在系统目录 pg_statistic
中。当使用 ANALYZE
和 VACUUM ANALYZE
命令时,pg_statistic
中的条目会被更新,并且即使刚刚更新也始终是近似的。
手动检查统计信息时,不要直接查看 pg_statistic
,而应查看它的视图 pg_stats
。 pg_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_vals
和 histogram_bounds
数组中每列的最大条目数,可以通过使用 ALTER TABLE SET STATISTICS
命令逐列设置,也可以通过设置 default_statistics_target 配置变量进行全局设置。现在的默认限制是 100 个条目。提高限制可能会让规划器估计更加准确,尤其是对于数据分布不规则的列,但代价是消耗更多 pg_statistic
中的空间并且计算估计需要更多的时间。相反,对于数据分布简单的列来说,一个较低的限制可能会足够。
可以在 第 68 章 中找到规划器如何使用统计信息的更多详细信息。
看到由于查询子句中使用的多列相关而导致慢查询运行错误的执行计划很常见。规划器通常假设多个条件相互独立,这在前值相关的列值时并不成立。常规统计信息因为其按独立列的性质无法捕获任何有关列间相关的信息。但是,PostgreSQL 有能力计算出可以捕获此信息的多变量统计信息。
因为列组合的数量很大,所以自动计算多变量统计信息是不切实际的。相反,可以创建扩展统计对象(通常直接称作统计对象)来指示服务器在相关的列集合中获取统计信息。
可以使用 CREATE STATISTICS
命令创建统计对象。创建这样的对象只是创建了一个目录条目,以表示对统计信息的兴趣。实际的数据收集是由 ANALYZE
执行的(无论是手动命令还是后台自动分析)。收集的值可以在 pg_statistic_ext_data
目录中进行检查。
ANALYZE
根据用于计算常规单列统计信息的表格行样本计算扩展统计信息。由于可以通过增加表格或其任何列的统计信息目标来增加样本量(如上一部分中所述),所以较高的统计信息目标通常会导致更准确的扩展统计信息,并且还会花费更多时间进行计算。
以下小节介绍了当前支持的多种类扩展统计信息。
扩展统计数据的最简单类型跟踪函数式依赖,即数据库范式定义中使用的概念。当a
列的值足够判断b
列的值时,我们称b
列函数式依赖于a
列,即没有两行具有相同的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%)由多个邮政编码表示。
在计算涉及函数式相关列的查询的选择度时,规划器使用相关系数调整每个条件选择度估计,以免产生低估。
目前仅在考虑将列与常量值进行比较的简单相等条件时才应用函数依赖关系,且IN
子句包含常量值。它们不会用于改善在比较两列或将列与表达式进行比较时的相等条件估计,也不用于范围子句、LIKE
或任何其他类型的条件。
在利用函数依赖关系进行估计时,规划器会假定涉及列上的条件兼容,因而多余。如果它们不兼容,正确的估计为零行,但未考虑这种可能性。例如,给定如下查询
SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '94105';
规划器将忽略city
子句,因为该子句不会改变选择性,这是正确的。然而,它将对以下内容做出同样假设
SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '90210';
虽然实际上将没有行满足此查询。然而,函数依赖性统计信息没有提供足够的信息来推断这一点。
在许多实际情况下,通常会满足此假定;例如,应用程序中可能有一个 GUI,它仅允许选择兼容的城市和邮编值用于查询中。但是,如果情况并非如此,则函数依赖关系可能不是可行的选项。
单列统计信息存储每列中的不同值数量。规划器仅具有单列统计数据时,当合并多列时(例如,对于GROUP BY a, b
)通常会出现不同值数量的错误估计,从而导致它选择错误的计划。
为了改善此类估计,ANALYZE
可以收集列组的 n 个不同统计信息。和以前一样,对每种可能的列分组执行此操作并不切实际,因此仅针对 statistics 对象中共同出现的那些列组收集数据,该对象通过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
周期就会浪费。
为每列存储的另一类统计信息是最常见值列表。这允许对单个列进行非常准确的估计,但可能会导致对多个列进行条件查询时的明显估计错误。
为改进此类估计,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)
这表示最常见的城市和州组合是华盛顿特区,实际频率(在样本中)约为 0.35%。该组合的基本频率(根据按列计算的简单频率)仅为 0.0027%,导致两个数量级的低估。
建议您创建MCV统计对象仅适用于实际一起使用条件的列组合,并且该组合的组数估计错误会导致计划不当。否则,ANALYZE
和规划周期就浪费了。