正如我们在上一节中看到的,查询规划器需要估算查询检索的行数,以便对查询计划做出良好的选择。本节快速介绍系统用于这些估算的统计信息。
统计信息的一个组成部分是每个表和索引中的条目总数,以及每个表和索引占用的磁盘块数。此信息保存在表 pg_class
中的列 reltuples
和 relpages
中。我们可以使用类似于以下内容的查询来查看它
SELECT relname, relkind, reltuples, relpages FROM pg_class WHERE relname LIKE 'tenk1%'; relname | relkind | reltuples | relpages ----------------------+---------+-----------+---------- tenk1 | r | 10000 | 358 tenk1_hundred | i | 10000 | 30 tenk1_thous_tenthous | i | 10000 | 30 tenk1_unique1 | i | 10000 | 30 tenk1_unique2 | i | 10000 | 30 (5 rows)
在这里,我们可以看到 tenk1
包含 10000 行,其索引也是如此,但索引(不出所料)比表小得多。
出于效率原因,reltuples
和 relpages
不会实时更新,因此它们通常包含一些过时的值。它们由 VACUUM
、ANALYZE
和一些 DDL 命令(例如 CREATE INDEX
)更新。未扫描整个表的 VACUUM
或 ANALYZE
操作(通常如此)将根据所扫描表的这一部分逐步更新 reltuples
计数,从而得到近似值。无论如何,规划器都会调整在 pg_class
中找到的值,以匹配当前物理表大小,从而获得更接近的近似值。
由于限制要检查的行数的 WHERE
子句,大多数查询只检索表中的一部分行。因此,规划器需要对 WHERE
子句的选择性进行估计,即匹配 WHERE
子句中每个条件的行数的比例。用于此任务的信息存储在 pg_statistic
系统目录中。pg_statistic
中的条目由 ANALYZE
和 VACUUM ANALYZE
命令更新,即使是最新更新的条目也始终是近似的。
与其直接查看 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.363388 | I- 580 Ramp+ | | | I- 880 Ramp+ | | | Sp Railroad + | | | I- 580 + | | | I- 680 Ramp name | t | -0.284859 | I- 880 Ramp+ | | | I- 580 Ramp+ | | | I- 680 Ramp+ | | | I- 580 + | | | State Hwy 13 Ramp (2 rows)
请注意,同一列显示了两行,一行对应于从 road
表开始的完整继承层次结构(inherited
=t
),另一行只包括 road
表本身(inherited
=f
)。
由 ANALYZE
存储在 pg_statistic
中的信息量,特别是 most_common_vals
和 histogram_bounds
数组中每列的最大条目数,可以使用 ALTER TABLE SET STATISTICS
命令按列设置,或通过设置 default_statistics_target 配置变量全局设置。当前的默认限制是 100 个条目。提高限制可能会生成更准确的规划器估计值,特别是对于数据分布不规则的列,但代价是消耗 pg_statistic
中更多的空间,并且计算估计值所需的时间略长。相反,对于数据分布简单的列,较低的限制可能就足够了。
有关规划器如何使用统计信息的更多详细信息,请参阅 第 76 章。
通常会看到运行不良执行计划的慢查询,因为查询子句中使用的多列相关。规划器通常假设多个条件彼此独立,而当列值相关时,这一假设不成立。常规统计信息由于其按单个列的性质,无法获取有关跨列关联的任何知识。但是,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-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
周期只是浪费。
为每列存储的另一种类型的统计数据是最常见值列表。这允许对各个列进行非常准确的估计,但可能会导致对具有多个列条件的查询进行重大错误估计。
为了改进此类估计,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
和规划周期只是浪费。