PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

69.2. 多变量统计信息示例 #

69.2.1. 函数依赖
69.2.2. 多变量 N-Distinct 计数
69.2.3. MCV 列表

69.2.1. 函数依赖 #

多变量相关性可以用一个非常简单的数据集来演示 — 一个包含两列且 两列取值相同的表:

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 个不同的值,且均匀分布。

下面的示例展示了对 a 列施加 WHERE 条件的估算结果:

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

69.2.2. 多变量 N-Distinct 计数 #

在估算多列集合的基数时会出现类似的问题,例如 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)

69.2.3. MCV 列表 #

如 Section 69.2.1 中所解释的,函数依赖 是一种非常廉价且高效的统计类型,但它的主要局限在于其全局性质(只在 列的层面跟踪依赖关系,而不跟踪单个列值之间的依赖)。

本节介绍 MCV(最常见值)列表的多变量变体,它是 Section 69.1 中描述的每列统计信息的直接 扩展。这类统计通过存储具体的值来克服上述局限,但自然也更昂贵,无论 是在 ANALYZE 中构建统计信息,还是在存储和规划 时间上。

我们再来看 Section 69.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 列表。

可以使用 pg_mcv_list_items 集合返回函数来检查 MCV 列表。

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 列表 的各个条目上,然后把匹配条目的频率求和。详情参见 src/backend/statistics/mcv.c 中的 mcv_clauselist_selectivity。

与函数依赖相比,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