TiDB 原始碼閱讀系列文章(十四)統計資訊(下)

PingCAP發表於2018-07-19

統計資訊(上) 中,我們介紹了統計資訊基本概念、TiDB 的統計資訊收集/更新機制以及如何用統計資訊來估計運算元代價,本篇將會結合原理介紹 TiDB 的原始碼實現。

文內會先介紹直方圖和 Count-Min(CM) Sketch 的資料結構,然後介紹 TiDB 是如何實現統計資訊的查詢、收集以及更新的。

資料結構定義

直方圖的定義可以在 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 函式中,我們實現了蓄水池抽樣演算法,用來生成均勻抽樣集合。由於其原理和程式碼都比較簡單,在這裡不再介紹。

取樣完成後,在 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 會探索多列統計資訊的收集和維護,為優化器提供更準確的統計資訊。

作者:謝海濱

相關文章