在PostgreSQL中CREATE STATISTICS

若是你用Postgres作了一些性能调优,你可能用过EXPLAIN。EXPLAIN向你展现了PostgreSQL计划器为所提供的语句生成的执行计划,它显示了语句所引用的表如何被扫描(使用顺序扫描、索引扫描等)。它显示了语句所引用的表将如何被扫描(使用顺序扫描,索引扫描等),以及若是使用多个表,将使用什么链接算法。可是,Postgres是如何提出这些计划的呢?算法

决定使用哪一种计划的一个很是重要的输入是计划员收集的统计数据。这些统计数据让计划员可以估计在执行计划的某一部分后会返回多少行,而后影响将使用的扫描或链接算法的种类。它们主要是经过运行ANALYZE或VACUUM(以及一些DDL命令,如CREATE INDEX)来收集/更新的。数据库

这些统计数据被规划者存储在pg_class和pg_statistics中。Pg_class基本上存储了每一个表和索引的总条目数,以及它们占用的磁盘块数。Pg_statistic存储的是每一列的统计数据,好比该列有多少%的值是空的,最多见的值是什么,直方图界限等。在下面的表格中,你能够看到Postgres为col1收集到的统计数据的例子。下面的查询输出显示,planner(正确)估计表中col1列有1000个不一样的值,还对最多见的值、频率等进行了其余估计。服务器

请注意,咱们已经查询了pg_stats(一个持有更可读的列统计版本的视图)。oop

CREATE TABLE tbl ( col1 int, col2 int ); INSERT INTO tbl SELECT i/10000, i/100000                                                  
FROM generate_series (1,10000000) s(i); ANALYZE tbl; select * from pg_stats where tablename = 'tbl' and attname = 'col1'; -[ RECORD 1 ]----------+---------------------------------------------------------------------------------
schemaname             | public tablename | tbl attname | col1 inherited | f null_frac | 0 avg_width | 4 n_distinct | 1000 most_common_vals | {318,564,596,...} most_common_freqs | {0.00173333,0.0017,0.00166667,0.00156667,...} histogram_bounds | {0,8,20,30,39,...} correlation | 1 most_common_elems | most_common_elem_freqs | elem_count_histogram | 

 

当单列统计不够用的时候
这些单列统计有助于planner估计条件的选择性(这就是planner用来估计索引扫描将选择多少行的缘由)。当在查询中提供了多个条件时,planner会假设这些列(或where子句条件)是相互独立的。当列之间相互关联或相互依赖时,这就不成立了,这将致使规划者低估或高估这些条件所返回的行数。性能

下面咱们来看几个例子。为了使计划简单易读,咱们经过设置max_parallel_workers_per_gather为0来关闭每一个查询的并行性。spa

 

EXPLAIN ANALYZE SELECT * FROM tbl where col1 = 1; QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Seq Scan on tbl  (cost=0.00..169247.80 rows=9584 width=8) (actual time=0.641..622.851 rows=10000 loops=1) Filter: (col1 = 1) Rows Removed by Filter: 9990000 Planning time: 0.051 ms Execution time: 623.185 ms (5 rows)

正如你在这里看到的,planner估计col1的值为1的行数为9584,而查询返回的实际行数为10000。因此,很是准确。code

可是,当你在第1列和第2列上都包含过滤器时,会发生什么呢?对象

EXPLAIN ANALYZE SELECT * FROM tbl where col1 = 1 and col2 = 0; QUERY PLAN                                                
----------------------------------------------------------------------------------------------------------
 Seq Scan on tbl  (cost=0.00..194248.69 rows=100 width=8) (actual time=0.640..630.130 rows=10000 loops=1) Filter: ((col1 = 1) AND (col2 = 0)) Rows Removed by Filter: 9990000 Planning time: 0.072 ms Execution time: 630.467 ms (5 rows)

 

planner的估算已经偏离了100倍! 让咱们试着了解一下为何会出现这种状况。blog

第一列的选择性大约是0.001(1/1000),第二列的选择性是0.01(1/100)。为了计算被这2个 "独立 "条件过滤的行数,planner将它们的选择性相乘。因此,咱们获得排序

选择性 = 0. 001 * 0. 01 = 0. 00001.

当这个乘以咱们在表中的行数即10000000时,咱们获得100。这就是planner估计的100的由来。可是,这几列不是独立的,咱们怎么告诉planner呢?


在PostgreSQL中CREATE STATISTICS
在Postgres 10以前,并无一个简单的方法来告诉计划员收集统计数据,从而捕捉到列之间的这种关系。可是,在Postgres 10中,有一个新的功能正是为了解决这个问题而创建的。CREATE STATISTICS能够用来建立扩展的统计对象,它能够告诉服务器收集关于这些有趣的相关列的额外统计。

功能依赖性统计
回到咱们以前的估算问题,问题是col2的值其实不过是col 1 / 10。在数据库术语中,咱们会说col2在功能上依赖于col1。这意味着col1的值足以决定col2的值,不存在两行col1的值相同而col2的值不一样的状况。所以,col2上的第2个过滤器实际上并无删除任何行!可是,planner捕捉到了足够的统计数据。可是,规划者捕捉到了足够的统计数据来知道这一点。

让咱们建立一个统计对象来捕获关于这些列的功能依赖统计,并运行ANALYZE。

 

CREATE STATISTICS s1 (dependencies) on col1, col2 from tbl; ANALYZE tbl; 让咱们看看planner如今拿出了什么。 EXPLAIN ANALYZE SELECT * FROM tbl where col1 = 1 and col2 = 0; QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Seq Scan on tbl  (cost=0.00..194247.76 rows=9584 width=8) (actual time=0.638..629.741 rows=10000 loops=1) Filter: ((col1 = 1) AND (col2 = 0)) Rows Removed by Filter: 9990000 Planning time: 0.115 ms Execution time: 630.076 ms (5 rows) 好多了! 咱们来看看是什么帮助planner作出了这个决定。

SELECT stxname, stxkeys, stxdependencies
FROM pg_statistic_ext
WHERE stxname = 's1';
stxname | stxkeys | stxdependencies
---------+---------+----------------------
s1 | 1 2 | {"1 => 2": 1.000000}
(1 row)

 
 

从这一点来看,咱们能够看到Postgres意识到col1彻底决定了col2,所以有一个系数为1来捕捉这些信息。如今,全部对这两列进行过滤的查询都会有更好的估计。

 

 

差别化统计
功能依赖性是你能够捕获列之间的一种关系。另外一种你能够捕捉的统计是一组列的不一样值的数量。咱们在前面提到过,planner捕捉到的是每一列的独特值数的统计,可是当组合多于一列时,这些统计又常常出错。

何时有很差的独特统计会伤害到我呢?让咱们来看一个例子。

EXPLAIN ANALYZE SELECT col1,col2,count(*) from tbl group by col1, col2; QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 GroupAggregate  (cost=1990523.20..2091523.04 rows=100000 width=16) (actual time=2697.246..4470.789 rows=1001 loops=1) Group Key: col1, col2 ->  Sort  (cost=1990523.20..2015523.16 rows=9999984 width=8) (actual time=2695.498..3440.880 rows=10000000 loops=1) Sort Key: col1, col2 Sort Method: external sort Disk: 176128kB ->  Seq Scan on tbl  (cost=0.00..144247.84 rows=9999984 width=8) (actual time=0.008..665.689 rows=10000000 loops=1) Planning time: 0.072 ms Execution time: 4494.583 ms

 

聚合行时,Postgres会选择作哈希聚合或分组聚合。若是它能在内存中装下哈希表,它就选择哈希聚合,不然它选择将全部的行进行排序,而后根据col1,col2进行分组。

如今,planner估计组的数量(等于col1,col2的不一样值的数量)将是100000。它看到它没有足够的work_mem来存储这个哈希表在内存中。因此,它使用基于磁盘的排序来运行查询。然而,在计划的实际部分能够看到,实际行数只有1001。而也许,咱们有足够的内存将它们装入内存,并进行哈希聚合。

咱们让计划员抓取n_distinct统计,从新运行查询,就知道了。

 

CREATE STATISTICS s2 (ndistinct) on col1, col2 from tbl; ANALYZE tbl; EXPLAIN ANALYZE SELECT col1,col2,count(*) from tbl group by col1, col2; QUERY PLAN                                                       
-----------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=219247.63..219257.63 rows=1000 width=16) (actual time=2431.767..2431.928 rows=1001 loops=1) Group Key: col1, col2 ->  Seq Scan on tbl  (cost=0.00..144247.79 rows=9999979 width=8) (actual time=0.008..643.488 rows=10000000 loops=1) Planning time: 0.129 ms Execution time: 2432.010 ms (5 rows) 你能够看到,如今的估计值更加准确了(即1000),查询速度也快了2倍左右。咱们能够经过运行下面的查询,看看planner学到了什么。 SELECT stxkeys AS k, stxndistinct AS nd FROM pg_statistic_ext WHERE stxname = 's2'; k | nd -----+----------------
 1 2 | {"1, 2": 1000}

 

现实的影响
在实际的生产模式中,你总会有某些列,它们之间有依赖关系或关系,而数据库并不知道。咱们在云端的Citus开源和Citus客户中看到的一些例子是。

有月、季、年的列,由于你想在报表中显示全部分组的统计数据。
地理层次结构之间的关系。例如,拥有国家、州和城市列,并经过它们进行过滤/分组。
这里的例子在数据集中只有10M行,咱们已经看到,在有相关列的状况下,使用CREATE统计能够显著改善计划,也显示出性能的提升。咱们有用户存储了数十亿行的数据,糟糕的计划会带来巨大的影响。在咱们的例子中,当计划员选择了一个糟糕的计划时,咱们不得不对10M行进行基于磁盘的排序,想象一下,若是有几十亿行的数据,会有多糟糕。