在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(一個持有更可讀的列統計版本的視圖)。

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來關閉每個查詢的並行性。

 

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。所以,非常準確。

但是,當你在第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倍! 讓我們試着瞭解一下爲什麼會出現這種情況。

第一列的選擇性大約是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行進行基於磁盤的排序,想象一下,如果有幾十億行的數據,會有多糟糕。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章