在 統計信息(上) 中,咱們介紹了統計信息基本概念、TiDB 的統計信息收集/更新機制以及如何用統計信息來估計算子代價,本篇將會結合原理介紹 TiDB 的源碼實現。git
文內會先介紹直方圖和 Count-Min(CM) Sketch 的數據結構,而後介紹 TiDB 是如何實現統計信息的查詢、收集以及更新的。github
直方圖的定義能夠在 histograms.go 中找到,值得注意的是,對於桶的上下界,咱們使用了在 《TiDB 源碼閱讀系列文章(十)Chunk 和執行框架簡介》 中介紹到 Chunk 來存儲,相比於用 Datum 的方式,能夠減小內存分配開銷。算法
CM Sketch 的定義能夠在 cmsketch.go 中找到,比較簡單,包含了 CM Sketch 的核心——二維數組 table
,並存儲了其深度與寬度,以及總共插入的值的數量,固然這些均可以直接從 table
中獲得。數據庫
除此以外,對列和索引的統計信息,分別使用了 Column 和 Index 來記錄,主要包含了直方圖,CM Sketch 等。 數組
在執行 analyze 語句時,TiDB 會收集直方圖和 CM Sketch 的信息。在執行 analyze 命令時,會先將須要 analyze 的列和索引在 builder.go 中切分紅不一樣的任務,而後在 analyze.go 中將任務下推至 TiKV 上執行。因爲在 TiDB 中也包含了 TiKV 部分的實現,所以在這裏仍是會以 TiDB 的代碼來介紹。在這個部分中,咱們會着重介紹直方圖的建立。數據結構
在統計信息(上)中提到,在創建列直方圖的時候,會先進行抽樣,而後再創建直方圖。框架
在 collect 函數中,咱們實現了蓄水池抽樣算法,用來生成均勻抽樣集合。因爲其原理和代碼都比較簡單,在這裏再也不介紹。less
採樣完成後,在 BuildColumn 中,咱們實現了列直方圖的建立。首先將樣本排序,肯定每一個桶的高度,而後順序遍歷每一個值 V:分佈式
若是 V 等於上一個值,那麼把 V 放在與上一個值同一個桶裏,不管桶是否是已經滿,這樣能夠保證每一個值只存在於一個桶中。函數
若是不等於上一個值,那麼判斷當前桶是否已經滿,就直接放入當前桶,並用 updateLastBucket 更改桶的上界和深度。
不然的話,用 AppendBucket 放入一個新的桶。
在創建索引列直方圖的時候,咱們使用了 SortedBuilder 來維護創建直方圖的中間狀態。因爲不能事先知道有多少行的數據,也就不能肯定每個桶的深度,不過因爲索引列的數據是已經有序的,因次咱們在 NewSortedBuilder 中將每一個桶的初始深度設爲 1。對於每個數據,Iterate 會使用創建列直方圖時相似的方法插入數據。若是在某一時刻,所需桶的個數超過了當前桶深度,那麼用 mergeBucket 將以前的每兩個桶合併爲 1 個,並將桶深擴大一倍,而後繼續插入。
在收集了每個 Region 上分別創建的直方圖後,還須要用 MergeHistogram 把每一個 Region 上的直方圖進行合併。在這個函數中:
爲了保證每一個值只在一個桶中,咱們處理了處理一下交界處桶的問題,即若是交界處兩個桶的上界和下界 相等,那麼須要先合併這兩個桶;
在真正合並前,咱們分別將兩個直方圖的平均桶深 調整 至大體相等;
若是直方圖合併以後桶的個數超過了限制,那麼把兩兩相鄰的桶 合二爲一。
在 統計信息(上) 中,咱們介紹了 TiDB 是如何更新直方圖和 CM Sketch 的。對於 CM Sketch 其更新比較簡單,在這裏再也不介紹。這個部分主要介紹一下 TiDB 是如何收集反饋信息和維護直方圖的。
統計信息(上)中提到,爲了避免去假設全部桶貢獻的偏差都是均勻的,須要收集每個桶的反饋信息,所以須要先把查詢的範圍按照直方圖桶的邊界切分紅不相交的部分。
在 SplitRange 中,咱們按照直方圖去切分查詢的範圍。因爲目前直方圖中的一個桶會包含上下界,爲了方便,這裏只按照上界去劃分,即這裏將第 i 個桶的範圍看作 (i-1 桶的上界,i 桶的上界]
。特別的,對於最後一個桶,將其的上界視爲無窮大。比方說一個直方圖包含 3 個桶,範圍分別是: [2,5],[8,8],[10,13],查詢的範圍是 (3,20],那麼最終切分獲得的查詢範圍就是 (3,5],(5,8],(8,20]。
將查詢範圍切分好後,會被存放在 QueryFeedback 中,以便在每一個 Region 的結果返回時,調用 Update 函數來更新每一個範圍所包含的 key 數目。注意到這個函數須要兩個參數:每一個 Region 上掃描的 start key 以及 Region 上每個掃描範圍輸出的 key 數目 output counts,那麼要如何更新 QueryFeedback
中每一個範圍包含的 key 的數目呢?
繼續以劃分好的 (3,5],(5,8],(8,20] 爲例,假設這個請求須要發送到兩個 region 上,region1 的範圍是 [0,6),region2 的範圍是 [6,30),因爲 coprocessor 在發請求的時候還會根據 Region 的範圍切分 range,所以 region1 的請求範圍是 (3,5],(5,6),region2 的請求範圍是 [6,8],(8,20]。爲了將對應的 key 數目更新到 QueryFeedback 中,須要知道每個 output count 對應的查詢範圍。注意到 coprocessor 返回的 output counts 其對應的 Range 都是連續的,而且同一個值只會對應一個 range,那麼咱們只須要知道第一個 output count 所對應的 range,即只須要知道此次掃描的 start key 就能夠了。舉個例子,對於 region1 來講,start key 是 3,那麼 output counts 對應的 range 就是 (3,5],(5,8],對 region2 來講,start key 是 6,output countshangyipians 對應的 range 就是 (5,8],(8,20]。
在收集了 QueryFeedback
後,咱們就能夠去使用 UpdateHistogram 來更新直方圖了。其大致上能夠分爲分裂與合併。
在 splitBuckets 中,咱們實現了直方圖的分裂:
首先,因爲桶與桶之間的反饋信息不相關,爲了方便,先將 QueryFeedback
用 buildBucketFeedback 拆分了每個桶的反饋信息,並存放在 BucketFeedback 中。
接着,使用 getSplitCount 來根據可用的桶的個數和反饋信息的總數來決定分裂的數目。
對於每個桶,將能夠分裂的桶按照反饋信息數目的比例均分,而後用 splitBucket 來分裂出須要的桶的數目:
首先,getBoundaries 會每隔幾個點取一個做爲邊界,獲得新的桶。
而後,對於每個桶,refineBucketCount 用與新生成的桶重合部分最多的反饋信息更新桶的深度。
值得注意的是,在分裂的時候,若是一個桶太小,那麼這個桶不會被分裂;若是一個分裂後生成的桶太小,那麼它也不會被生成。
在桶的分裂完成後,咱們會使用 mergeBuckets 來合併桶,對於那些超過:
在分裂的時候,會記錄每個桶是否是新生成的,這樣,對於原先就存在的桶,用 getBucketScore 計算合併的以後產生的偏差,令第一個桶佔合併後桶的比例爲 r,那麼令合併後產生的偏差爲 abs(合併前第一個桶的高度 - r * 兩個桶的高度和)/ 合併前第一個桶的高度。
接着,對每一桶的合併的偏差進行排序。
最後,按照合併的偏差從下到大的順序,合併須要的桶。
在查詢語句中,咱們經常會使用一些過濾條件,而統計信息估算的主要做用就是估計通過這些過濾條件後的數據條數,以便優化器選擇最優的執行計劃。
因爲在單列上的查詢比較簡單,這裏再也不贅述,代碼基本是按照 統計信息(上) 中的原理實現,感興趣能夠參考 histogram.go/lessRowCount 以及 cmsketch.go/queryValue。
統計信息(上)中提到,Selectivity 是統計信息模塊對優化器提供的最重要的接口,處理了多列查詢的狀況。Selectivity 的一個最重要的任務就是將全部的查詢條件分紅儘可能少的組,使得每一組中的條件均可以用某一列或者某一索引上的統計信息進行估計,這樣咱們就能夠作儘可能少的獨立性假設。
須要注意的是,咱們將單列的統計信息分爲 3 類:indexType 即索引列,pkType 即 Int 類型的主鍵,colType 即普通的列類型,若是一個條件能夠同時被多種類型的統計信息覆蓋,那麼咱們優先會選擇 pkType 或者 indexType。
在 Selectivity 中,有以下幾個步驟:
getMaskAndRange 爲每一列和每個索引計算了能夠覆蓋的過濾條件,用一個 int64 來當作一個 bitset,並把將該列能夠覆蓋的過濾條件的位置置爲 1。
接下來在 getUsableSetsByGreedy 中,選擇儘可能少的 bitset,來覆蓋儘可能多的過濾條件。每一次在尚未使用的 bitset 中,選擇一個能夠覆蓋最多還沒有覆蓋的過濾條件。而且若是能夠覆蓋一樣多的過濾條件,咱們會優先選擇 pkType 或者 indexType。
用統計信息(上)提到的方法對每個列和每個索引上的統計信息進行估計,並用獨立性假設將它們組合起來當作最終的結果。
統計信息的收集和維護是數據庫的核心功能,對於基於代價的查詢優化器,統計信息的準確性直接影響了查詢效率。在分佈式數據庫中,收集統計信息和單機差異不大,可是維護統計信息有比較大的挑戰,好比怎樣在多節點更新的狀況下,準確及時的維護統計信息。
對於直方圖的動態更新,業界通常有兩種方法:
對於每一次增刪,都去更新對應的桶深。在一個桶的桶深太高的時候分裂桶,通常是把桶的寬度等分,不過這樣很難準確的肯定分界點,引發偏差。
使用查詢獲得的真實數去反饋調整直方圖,假定全部桶貢獻的偏差都是均勻的,用連續值假設去調整全部涉及到的桶。然而偏差均勻的假設經常會引發問題,好比噹噹新插入的值大於直方圖的最大值時,就會把新插入的值引發的偏差分攤到直方圖中,從而引發偏差。
目前 TiDB 的統計信息仍是以單列的統計信息爲主,爲了減小獨立性假設的使用,在未來 TiDB 會探索多列統計信息的收集和維護,爲優化器提供更準確的統計信息。
做者:謝海濱