postgresql統計信息機制分析
為什么要記錄統計信息(why)
這里提的統計信息主要是用於選擇執行計划的統計信息,不是對系統的監控。
一條SQL在PG中的執行過程是:
----> SQL輸入
----> 解析SQL,獲取解析后的語法樹
----> 分析、重寫語法樹,獲取查詢樹
----> 根據重寫、分析后的查詢樹計算各路徑代價,從而選擇一條成本最優的執行樹
----> 根據執行樹進行執行
----> 獲取結果並返回
上圖中,生成查詢樹后,就需要根據統計信息預判生成的各個執行計划的執行代價,這里的代價主要是計算IO代價。即讀頁面的開銷,這可以認為讀取的頁面數和行數是正相關的。
統計信息主要記錄的就是表的行數頁面以及不同列不同值的分布關系。
看一個例子
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;
QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=24.06..394.64 rows=1007 width=244)
Recheck Cond: (unique1 < 1000)
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..23.80 rows=1007 width=0)
Index Cond: (unique1 < 1000)
可以看到這個例子會列出不同的算子的預估代價。
記錄什么統計信息(what)
根據上面的例子,我們可以推斷,統計信息至少要記錄行數和頁數
SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'tenk1%'; relname | relkind | reltuples | relpages ----------------------+---------+-----------+---------- tenk1 | r | 10000 | 358 tenk1_hundred | i | 10000 | 30 tenk1_thous_tenthous | i | 10000 | 30 tenk1_unique1 | i | 10000 | 30 tenk1_unique2 | i | 10000 | 30 (5 rows)
如果查詢語句包括where這類條件語句,就像上文的例子WHERE unique1 < 1000
,那我們還需要記錄數據的分布律,來預計小於1000到底存在多少行的數據,如果大部分數據都小於1000,那么順序掃描的開銷要低於索引掃描。因此pg還統計了以下信息。
Name | Type | References | Description |
---|---|---|---|
starelid |
oid |
pg_class.oid |
The table or index that the described column belongs to |
staattnum |
int2 |
pg_attribute.attnum |
The number of the described column |
stainherit |
bool |
If true, the stats include inheritance child columns, not just the values in the specified relation | |
stanullfrac |
float4 |
The fraction of the column's entries that are null(空值的比例) | |
stawidth |
int4 |
The average stored width, in bytes, of nonnull entries(行平均長度) | |
stadistinct |
float4 |
The number of distinct nonnull data values in the column. A value greater than zero is the actual number of distinct values. A value less than zero is the negative of a multiplier for the number of rows in the table; for example, a column in which about 80% of the values are nonnull and each nonnull value appears about twice on average could be represented by stadistinct = -0.4. A zero value means the number of distinct values is unknown.(重復率) |
|
stakindN |
int2 |
A code number indicating the kind of statistics stored in the N th "slot" of the pg_statistic row. (統計信息的類型) |
|
staopN |
oid |
pg_operator.oid |
An operator used to derive the statistics stored in the N th "slot". For example, a histogram slot would show the < operator that defines the sort order of the data.(用於表示該統計值支持的操作,如’=’或’<’等。) |
stanumbersN |
float4[] |
Numerical statistics of the appropriate kind for the N th "slot", or null if the slot kind does not involve numerical values(如果是MCV類型(即kind=1),那么這里即是下面對應的stavaluesN出現的概率值,即MC) |
|
stavaluesN |
anyarray |
Column data values of the appropriate kind for the N th "slot", or null if the slot kind does not store any data values. Each array's element values are actually of the specific column's data type, or a related type such as an array's element type, so there is no way to define these columns' type more specifically thananyarray .(統計信息的值,根據類型不同,值的內容也不同) |
PG為每列統計了五類信息,根據不同的數據類型會選擇統計不同的信息,具體的內容很復雜,有時間我會放在另一篇文章討論。
統計信息記錄在哪里(where)
用於進行查詢計划選擇的統計信息記錄在系統表內
- pg_statistic 記錄值的分布率
- pg_class 記錄行數和頁面數
誰來更新統計信息(who)
統計信息是有autovacuum線程來更新,PG使用MVCC機制進行數據庫的並發控制,因此同樣需要一組后台進程進行過期版本的清理。

autovacuum線程不止負責對過期元組進行清理,同時也負責定期更新表的統計信息。
為什么要把這兩個操作放在一起?
- PG的MVCC機制數據和數據的舊版本是統一存放在表文件上的,在清理時要進行全表掃描的操作,而統計信息的收集也是需要讀取表文件的,這兩個操作放在一起做可以在一定程度上節省IO;
- 清理廢舊元組和更新統計信息都是通過收集表的元組變更數據來觸發的,共享一套機制,因此放在一起處理也比較方便;
何時觸發統計信息(when)
統計信息有兩種觸發方式:
- 用戶使用analyze命令手動觸發(analyze/vacuum analyze)
- daemon進程觸發
講講daemon進程觸發的機制:
在PG中,事務提交/回滾時會發消息給進程pgstat,pgstat會匯總這份信息並記錄到文件中,autovacuum launcher會定期讀取文件,獲得,當某個表的改動超過閾值時便會觸發一次統計信息的更新操作。

需要注意的是autovacuum worker也會給pgstat發消息,但實際上這個消息是通知pgstat統計更新/清理已經完成,可以清理統計信息了。因此stat file只有pgstat更新,launcher只是讀取,不涉及並發寫沖突。
當變化元組數超過多少時啟動一次統計信息更新?PG采用如下機制:
anlthresh = (float4) anl_base_thresh + anl_scale_factor * reltuples;
anl_base_thresh默認值時50,anl_scale_factor默認值時0.1,這都是可配置的,而且是每個表可以獨立配置的。anl_base_thresh的作用是如果表文件太小,比如只有5行,如果不設置anl_base_thresh的話,1行變化就會啟動統計信息更新,這是沒有必要的。當然,只有一個頁面的表默認就會使用順序掃描。
如何記錄統計信息(how)
抽樣算法
關於抽樣算法,這里有一個很好的說明,如下:摘自阿里月報
-
確定這個字段是否可以分析,如果不可以,則返回NULL。
一般有兩種情況致使這個字段不進行分析:字段已被刪除(已刪除的字段還存在於系統表中,只是作了標記);用戶指定了字段。 -
獲取數據類型,並決定針對該類型的采樣數據量和統計函數
不同的類型,其分析函數也不同,比如array_typanalyze。如果該類型沒有對應的分析函數,則采用標准的分析函數std_typanalyze。
以標准分析函數為例,其確定了兩個地方:采樣后用於統計的函數(compute_scalar_stats或compute_minimal_stats,和采樣的記錄數(現在默認是300 * 100)。 -
索引
索引在PG中,是以與表類似的方式存在的。當analyze沒有指定字段,或者是繼承表的時候,也會對索引進行統計信息的計算。以AccessShareLock打開該表上所有的鎖,同樣的檢查索引的每個字段是否需要統計、如何統計等。 -
采樣
選擇表所有字段所需采樣數據量的最大值作為最終采樣的數據量。當前PG采取的兩階段采樣的算法:- 先獲取所需數據量的文件塊
- 遍歷這些塊,根據Vitter算法,選擇出所需數據量的記錄時以頁為單位,盡量讀取該頁中所有的完整記錄,以減少IO;按照物理存儲的位置排序,后續會用於計算相關性(correlation)。
這里的采樣並不會處理事務中的記錄,如正在插入或刪除的記錄。但如果刪除或插入操作是在當前analyze所在的事務執行的,那么插入的是被記為live_tuples並且加入統計的;刪除的會被記為dead_tuples而不加入統計。
由此會可能產生兩個問題:
- 當有另外一個連接正好也在進行統計的時候,自然會產生不同的統計值,且后來者會直接覆蓋前者。當統計期間有較多的事務在執行,且很快結束,那么結果與實際情況可能有點差別。
- 當有超長的事務出現,當事務結束時,統計信息與實際情況可能有較大的差距。
以上兩種情況,重復執行analyze即可。但有可能因統計信息不准確導致的執行計划異常而造成短時間的性能波動,需要注意!這里也說明了長事務的部分危害。
-
統計、計算
在獲取到相應樣本數據后,針對每個字段分別進行分析。
首先會依據當前字段的值,對記錄進行排序。因在取出樣本數據的時候,按照tuple在磁盤中的位置順序取出的,因此對值進行排序后即可計算得出相關性。另外,在排序后,也更容易計算統計值的頻率,從而得出MCV和MCF。這里采用的快速排序!
之后,會根據每個值進行分析:-
如果是NULL,則計數
NULL概率的計算公式是:stanullfrac = null_number / sample_row_number。 -
如果是變長字段,如text等,則需要計算平均寬度
-
計算出現最多的值,和相應頻率
-
基數的計算
部分計算稍微復雜一些,分為以下三種情況:
-
當采樣中的值沒有重復的時候,則認為所有的值唯一,stadistinct = -1。
-
當采樣中的每個值都出現重復的時候,則認為基數有限,則stadistinct = distinct_value_number
-
當采樣中的值中,存在有唯一值並且存在不唯一值的時候,則依據以下的公式(by Haas and Stokes in IBM Research Report RJ 10025):
n * d / (n - f1 + f1 * n/N)
其中,N是指所有的記錄數,即pg_class.reltuples;n是指sample_row_number,即采樣的記錄數;f1則是只出現一次的值的數據;d則是采樣中所有的值的數量。
-
-
MCV / MCF
並不是所有采樣的值都會被列入MCV/MCF。首先是如果可以,則將所有采樣的記錄放到MCV中,如表所有的記錄都已經取作采樣的時候;其次,則是選取那些出現頻率超過平均值的值,事實上是超過平均值的25%;那些出現頻率大於直方圖的個數的倒數的時候等。 -
直方圖
計算直方圖,會首先排除掉MCV中的值。
意思是直方圖中的數據不包含MCV/MCF的部分,兩者的值是補充關系而且不會重合,但不一定互補(兩種加起來未必是全部數據)。這個也與成本的計算方式有關系,請參考row-estimation-examples 。
其計算公式相對比較簡單,如下:values[(i * (nvals - 1)) / (num_hist - 1)]
i指直方圖中的第幾列;nvals指當前還有多少個值;num_hist則指直方圖中還有多少列。計算完成后,kind的值會被置為2。
到此,采樣的統計基本結束。
-
樣本大小
PG采樣大小來自論文《Random sampling for histogram construction: how much is enough?》
論文內提出里一個公式
在表大小為n,矩形圖大小為k,分組內相關最大相關性錯誤為f,錯誤可能為gamma的情況下,最小的樣本大小如下:
r = 4 * k * ln(2*n/gamma) / f^2
取f=0.5,gamma=0.01,n=10^6,我們可以得到
r = 305.82 * k
可以看到,元組數對樣本大小的影響很小,即使元組數為10^12,采樣出錯的概率也不高,因此PG統一使用300作為采樣的權值,簡化問題,在確定樣本大小時無需獲取表大小。
注釋如下
/*-------------------- * The following choice of minrows is based on the paper * "Random sampling for histogram construction: how much is enough?" * by Surajit Chaudhuri, Rajeev Motwani and Vivek Narasayya, in * Proceedings of ACM SIGMOD International Conference on Management * of Data, 1998, Pages 436-447. Their Corollary 1 to Theorem 5 * says that for table size n, histogram size k, maximum relative * error in bin size f, and error probability gamma, the minimum * random sample size is * r = 4 * k * ln(2*n/gamma) / f^2 * Taking f = 0.5, gamma = 0.01, n = 10^6 rows, we obtain * r = 305.82 * k * Note that because of the log function, the dependence on n is * quite weak; even at n = 10^12, a 300*k sample gives <= 0.66 * bin size error with probability 0.99. So there's no real need to * scale for n, which is a good thing because we don't necessarily * know it at this point. *-------------------- */
隨機數算法
隨機數算法使用的是Knuth's Algorithm S,就不細說了,具體流程如下:
設k為樣本大小, K為抽樣對象大小, i為當前對象,V生成的0-1之間的隨機數
while i < k p = k/(K - i); if V < p pick it k--; i++
alter table的沖突處理
analyze是需要加鎖的,見下
#define NoLock 0 #define AccessShareLock 1 /* SELECT */ #define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */ #define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */ #define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL),ANALYZE, CREATE INDEX * CONCURRENTLY */ #define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */ #define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW * SHARE */ #define ExclusiveLock 7 /* blocks ROW SHARE/SELECT...FOR UPDATE */ #define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM FULL, * and unqualified LOCK TABLE */
analyze需要加ShareUpdateExclusiveLock
這個鎖和alter table是有沖突的,禁止了沖突的發生。
和MVCC機制的沖突處理
統計時會過濾掉部分元組,代碼如下。
switch (HeapTupleSatisfiesVacuum(&targtuple, OldestXmin, targbuffer)) { case HEAPTUPLE_LIVE: pick it case HEAPTUPLE_DEAD: case HEAPTUPLE_RECENTLY_DEAD: give up case HEAPTUPLE_INSERT_IN_PROGRESS: pick it only it is inserted by me; case HEAPTUPLE_DELETE_IN_PROGRESS: give up default: elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); break; }
- 統計已提交事務插入的元組;
- 統計本事務插入的元組;
關於第二點是為了處理某個事務插入了很多元組之后,立刻在本事務內進行分析的情況,比如很多包含臨時表的操作。
資源控制
vacuum worker在每次touch頁面時,會自動計數,統計為cost,當cost超過一定值時,會自動sleep幾秒鍾,降低vacuum和analyze對正常流程的影響。
這些參數是可以配置的,這里我們只談談autovacuum情況下的默認值。
- 每次cost超過200時,就會啟動一次sleep,默認80ms;
- page hit的cost是1,page miss的cost是10,page dirty的cost是20;
- 睡眠時間是按照如下公式動態計算的
msec = VacuumCostDelay(20ms) * VacuumCostBalance / VacuumCostLimit(200);
if (msec > VacuumCostDelay * 4) msec = VacuumCostDelay * 4;
例外
- 系統表不需要統計信息
- 系統列不需要統計信息
參考文檔
以上例子來自PG官方文檔
postgres源碼
https://www.postgresql.org/docs/9.6
http://mysql.taobao.org/monthly/2016/05/09/
https://www.postgresql.org/docs/9.6/static/row-estimation-examples.html
https://www.postgresql.org/docs/9.6/static/planner-stats-security.html
作者:文韜123456
鏈接:https://www.jianshu.com/p/d7b2034cc68d
來源:簡書