若是你用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行進行基於磁盤的排序,想象一下,若是有幾十億行的數據,會有多糟糕。