一、為什么需要 ANALYZE
首先介紹下 RBO 和 CBO,這是數據庫引擎在執行 SQL 語句時的2種不同的優化策略。
RBO(Rule-Based Optimizer)
基於規則的優化器,就是優化器在優化查詢計划的時候,是根據預先設置好的規則進行的,這些規則無法靈活改變。舉個例子,索引優先於掃描,這是一個規則,優化器在遇到所有可以利用索引的地方,都不會選擇掃描。這在多數情況下是正確的,但也不完全如此:
比如 一張個人信息表中性別欄目加上索引,由於性別是只有2個值的枚舉類,也就是常說的基數非常低的列,在這種列上使用索引往往效果還不如掃描
因此 RBO 的優化方式是死板的,粗放的,目前已逐漸被 CBO 方式取代。
CBO(Cost Based Optimizer)
基於代價的優化器,就是優化器在優化查詢計划的時候,是根據動態計算出來的 Cost(代價)來判斷如何進行選擇。那如何計算代價呢?這里一般是基於代價模型和統計信息,代價模型是否合理,統計信息是否准確都會影響優化的效果。
還是拿上面員工性別統計為例,在 CBO 的優化方式下,物理計划就不會選擇走索引。當然上面的例子比較簡單,在 Greenplum 運行的復雜 SQL 中,優化器最核心的還是在 scan 和 join 的各種實現方式中做出選擇,這才是能大幅提升性能的關鍵點。
前面提到 CBO 需要一個代價模型和統計信息,代價模型和規則一樣,需要預先設置好,那統計信息是如何收集的?多數基於 CBO 優化的計算引擎,包括 Greenplum,Oracle,Hive,Spark 等都類似,除了可以按一定規則自動收集統計信息外,還都支持手動輸入命令進行收集,通常這個命令都叫 ANALYZE。
結論:由於 CBO 優化的需求,因此我們需要使用 ANALYZE 命令去收集統計信息。
二、ANALYZE 怎么使用
說明
ANALYZE 是 Greenplum 提供的收集統計信息的命令。
ANALYZE 支持三種粒度,列,表,庫,如下:
限制
ANALYZE 會給目標表加 SHARE UPDATE EXCLUSIVE 鎖,也就是與 UPDATE,DELETE,還有 DDL 語句沖突。
速度
ANALYZE 是一種采樣統計算法,通常不會掃描表中所有的數據,但是對於大表,也仍會消耗一定的時間和計算資源。
采樣統計會有精度的問題,因此 Greenplum 也提供了一個參數 default_statistics_target,調整采樣的比例。簡單說來,這個值設置得越大,采樣的數量就越多,准確性就越高,但是消耗的時間和資源也越多。

default_statistics_target.png
直接修改服務器的參數會影響整個集群,通常不建議這樣操作。如果確實有需要,可以嘗試只修改某列的對應參數,如下:
時機
根據上文所述,ANALYZE 會加鎖並且也會消耗系統資源,因此運行命令需要選擇合適的時機盡可能少的運行。根據 Greenplum 官網建議,以下3種情況發生后建議運行 ANALYZE
-
批量加載數據后,比如 COPY
-
創建索引之后
-
INSERT, UPDATE, and DELETE 大量數據之后
自動化
除了手動運行,ANALYZE 也可以自動化。實際上默認情況下,我們對空表寫入數據后, Greenplum 也會自動幫我們收集統計信息,不過之后在寫入數據,就需要手動操作了。
有2個參數可以用來調整自動化收集的時機,gp_autostats_mode 和 gp_autostats_on_change_threshold。gp_autostats_mode 默認是 on_no_stats,也就是如果表還沒有統計信息,這時候寫入數據會導致自動收集,這之后,無論表數據變化多大,都只能手動收集了。如果將 gp_autostats_mode 修改為 on_change ,就是在數據變化量達到 gp_autostats_on_change_threshold 參數配置的量之后,系統就會自動收集統計信息。
分區表
Greenplum 官網對於分區表的 ANALYZE 專門進行了講解,其實只要保持默認值,不去修改系統參數 optimizer_analyze_root_partition,那么對於分區表的操作並沒有什么不同,直接在 root 表上進行 ANALYZE 即可,系統會自動把所有葉子節點的分區表的統計信息都收集起來。
如果分區表的數目很多,那在 root 表上進行 ANALYZE 可能會非常耗時,通常的分區表都是帶有時間維度的,歷史的分區表並不會修改,因此單獨 ANALYZE 數據發生變化的分區,是更好的實踐。
三、統計信息去了哪里
pg_class
表的大小是統計信息里面最直觀,也幾乎是最重要的,這個信息是放在 pg_catalog.pg_class 系統表中,reltuples 代表元組數(行數),relpages 代表實際占用的 page 數目(Greenplum中一個 page 為32KB)。
需要注意以下3點
1. reltuples 不是准確值,獲取表的准確行數還是需要 count。
2. reltuples 和 relpages 需要通過 ANALYZE 進行收集,對於已有數據的表,系統不會自動更新。
3. reltuples 和 relpages 不一定能對齊,比如條數看起來不多的表,實際占用的 page 數目很大,這種一般是由於數據膨脹(bloat)造成,這時候需要 vacuum 等操作。
pg_statistic
關於列的統計信息都是存放在 pg_catalog.pg_statistic 系統表中。其中表的每一列(如果有統計)都會有一行對應的數據。了解並掌握 pg_statistic 的內容,對於深入理解查詢優化非常重要。
列的統計信息內容很豐富,但是目的都是讓優化器估算出,一個查詢條件,能夠過濾多少數據。
以下列舉了 pg_statistic 的重要字段:

對於 stakindN 字段中的統計方式,這里選擇3個最常見的進行說明:
1. STATISTIC_KIND_MCV
高頻值,在一個列中出現最頻繁的值。
高頻值統計在很多場景下都有價值,這里舉一個數據傾斜的 hash join 例子,如下代碼:
hash join 場景下,我們需要盡可能的把 inner table 構建在內存中,但內存資源是有限的,因此我們需要做出一些選擇,什么內容優先放入內存中。如果外表有高頻值,那我們可以考慮把高頻值對應的內表信息優先放入到內存中,在實踐中,Greenplum 是單獨構建一個 skew hash table 與 main hash table 並存。
2. STATISTIC_KIND_HISTOGRAM
直方圖,使用等頻直方圖來描述一個列中的數據的分布。
直方圖主要用於數據分布不均勻的情況下,對按列過濾后能返回多少數據進行預估。
舉個例子,一個有3種產品的訂單表,商品 A 很熱銷,訂單量在90%,商品 B 一般,訂單量在9%,商品 C 只有1%,則該列的 NDV(Number of Distinct Value)值為3,如果一共有1000000條數據,在沒有直方圖統計的情況下,如果查詢商品 C 的訂單,優化器會預計要掃描1000000/3≈330000,因此可能選擇全表 scan,如果含有直方圖統計,優化器就知道實際上 C 商品可能就幾千條數據,因此會選擇走索引。當然這個例子很簡單,實際情況會復雜很多。
3. STATISTIC_KIND_CORRELATION
相關系數,記錄的是當前列未排序的數據分布和排序后的數據分布的相關性。
用於估算索引掃描代價的,統計值在-1到1,值越大,表示相關性越高,也就是使用索引掃描代價越低。
舉個例子,初始化如下2張表
在查看表對應的統計信息,可以看出在 number 列,你按升序寫入1000個數,該列物理存儲的數據實際上就是按升序排序的,反過來降序寫入1000個數,由於順序是相反的,所以相關性是-1

correlation.png
四、例子
以下將會構造一個大小表 join 的場景,來說明統計信息的收集對於查詢計划的影響。
1. 初始化表結構和數據:
pg_class 中對應的數據如下:

small_table.png

big_table.png
2. 大小表 join
注意為了構造小表廣播的場景,這里關聯鍵需要選擇非分布鍵。

explain1.png
3. 給小表插入數據
這里給小表插入數據后,小表的數據量超過大表
在沒有 ANALYZE 的情況下,pg_class 中的數據沒有發生變化,因此查詢計划也沒有發生變化。
4. 收集統計信息
運行 ANALYZE 收集小表的統計信息,如下:

new_small_table.png
在運行 join 語句,查詢計划發生變化:

explain2.png
結論:查詢優化器在收到新的統計信息之后,發現是2張數據量差不多的表進行 join,因此選擇重分布而不是小表廣播。

關注“騰訊雲大數據”公眾號,技術交流、最新活動、服務專享一站Get~