多元相关性可以通过一个非常简单的数据集来演示 — 一个包含两列的表,两列都含有相同的值:
CREATE TABLE t (a INT, b INT); INSERT INTO t SELECT i % 100, i % 100 FROM generate_series(1, 10000) s(i); ANALYZE t;
如Section 14.2所述,规划器可以利用从pg_class得到的页数和行数来确定t的基数:
SELECT relpages, reltuples FROM pg_class WHERE relname = 't';
relpages | reltuples
----------+-----------
45 | 10000
数据分布非常简单;每一列中都只有 100 个不同的值,并且均匀分布。
下面的示例展示了对WHERE条件在a列上进行估计的结果:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..170.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: (a = 1)
Rows Removed by Filter: 9900
规划器会检查该条件,并认定此子句的选择率为 1%。将该估计与实际行数相比,可以看出估计非常准确(实际上是精确的,因为这张表非常小)。把WHERE条件改为使用b列,会生成相同的计划。但请看如果把同一条件同时应用到两列上,并用AND连接,会发生什么:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-----------------------------------------------------------------------------
Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
规划器分别估计每个条件的选择率,得到与上面相同的 1% 估计值。然后它假定这些条件彼此独立,于是将它们的选择率相乘,得到最终只有 0.01% 的选择率估计。这是严重的低估,因为实际匹配这些条件的行数(100)要高出两个数量级。
这个问题可以通过创建一个统计信息对象来解决,该对象会指示ANALYZE在这两列上计算函数依赖的多元统计信息:
CREATE STATISTICS stts (dependencies) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
在估计多列集合的基数时,也会出现类似的问题,例如GROUP BY子句会生成多少个组。当GROUP BY只列出一列时,n-distinct 估计(可从 HashAggregate 节点估计返回的行数看出)非常准确:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a;
QUERY PLAN
-----------------------------------------------------------------------------------------
HashAggregate (cost=195.00..196.00 rows=100 width=12) (actual rows=100.00 loops=1)
Group Key: a
-> Seq Scan on t (cost=0.00..145.00 rows=10000 width=4) (actual rows=10000.00 loops=1)
但是如果没有多元统计信息,对于在GROUP BY中包含两列的查询,组数估计会像下面这样差一个数量级:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
QUERY PLAN
--------------------------------------------------------------------------------------------
HashAggregate (cost=220.00..230.00 rows=1000 width=16) (actual rows=100.00 loops=1)
Group Key: a, b
-> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000.00 loops=1)
通过重新定义统计信息对象,使其包含这两列的 n-distinct 计数,估计值就会大幅改进:
DROP STATISTICS stts;
CREATE STATISTICS stts (dependencies, ndistinct) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
QUERY PLAN
--------------------------------------------------------------------------------------------
HashAggregate (cost=220.00..221.00 rows=100 width=16) (actual rows=100.00 loops=1)
Group Key: a, b
-> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000.00 loops=1)
如Section 67.2.1中所述,函数依赖是一种非常廉价且高效的统计信息类型,但其主要局限在于它具有全局性(只在列层面跟踪依赖关系,而不是跟踪各个列值之间的依赖关系)。
本节介绍MCV(高频值)列表的多元变体,它是Section 67.1中按列统计信息的直接扩展。这类统计信息通过存储单个值来解决上述限制,但无论是在ANALYZE中构建统计信息,还是在存储和规划时间方面,它的代价自然都更高。
让我们再次查看Section 67.2.1中的查询,不过这一次是在同一组列上创建一个MCV列表(务必先删除函数依赖,以确保规划器使用新创建的统计信息)。
DROP STATISTICS stts;
CREATE STATISTICS stts2 (mcv) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
其估计与使用函数依赖时一样准确,这主要得益于该表相当小,而且分布简单、不同值数量也少。在查看第二个查询之前,我们先稍微检查一下MCV列表,因为函数依赖对那个查询的处理并不特别理想。
要检查MCV列表,可以使用返回集函数pg_mcv_list_items。
SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid),
pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts2';
index | values | nulls | frequency | base_frequency
-------+----------+-------+-----------+----------------
0 | {0, 0} | {f,f} | 0.01 | 0.0001
1 | {1, 1} | {f,f} | 0.01 | 0.0001
...
49 | {49, 49} | {f,f} | 0.01 | 0.0001
50 | {50, 50} | {f,f} | 0.01 | 0.0001
...
97 | {97, 97} | {f,f} | 0.01 | 0.0001
98 | {98, 98} | {f,f} | 0.01 | 0.0001
99 | {99, 99} | {f,f} | 0.01 | 0.0001
(100 rows)
这证实这两列中有 100 个不同的组合,而且它们都大致同样常见(每个组合的频率都是 1%)。基础频率是根据按列统计信息计算出的频率,相当于假设不存在多列统计信息。如果任一列中存在空值,它会在nulls列中标识出来。
在估计选择率时,规划器会将所有条件应用到MCV列表中的各个项上,然后把匹配项的频率加总起来。 详情请参阅mcv_clauselist_selectivity(位于src/backend/statistics/mcv.c中)。
与函数依赖相比,MCV列表有两大主要优点。 首先,列表存储的是真实值,因此可以判断哪些组合是兼容的。
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 10;
QUERY PLAN
---------------------------------------------------------------------------
Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0.00 loops=1)
Filter: ((a = 1) AND (b = 10))
Rows Removed by Filter: 10000
其次,MCV列表可以处理更广泛的子句类型,而不仅仅是像函数依赖那样只适用于等值子句。例如,考虑同一张表上的如下范围查询:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a <= 49 AND b > 49;
QUERY PLAN
---------------------------------------------------------------------------
Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0.00 loops=1)
Filter: ((a <= 49) AND (b > 49))
Rows Removed by Filter: 10000
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。