做者: 謝海濱git
在 TiDB 裏,SQL 優化的過程能夠分爲邏輯優化和物理優化兩個部分,在物理優化階段須要爲邏輯查詢計劃中的算子估算運行代價,並選擇其中代價最低的一條查詢路徑做爲最終的查詢計劃。這裏很是關鍵的一點是如何估算查詢代價,本文所介紹的統計信息是這個估算過程的核心模塊。github
這部份內容很是複雜,因此會分紅兩篇文章來介紹。本篇文章介紹統計信息基本概念、TiDB 的統計信息收集/更新機制以及如何用統計信息來估計算子代價。上篇側重於介紹原理,下篇會結合原理介紹 TiDB 的源碼實現。web
爲了獲得查詢路徑的執行代價,最簡單的辦法就是實際執行這個查詢計劃,不過這樣子作就失去了優化器的意義。不過,優化器並不須要知道準確的代價,只須要一個估算值,以便可以區分開代價差異較大的執行計劃。所以,數據庫經常會維護一些實際數據的歸納信息,用以快速的估計代價,這即是統計信息。算法
在 TiDB 中,咱們維護的統計信息包括表的總行數,列的等深直方圖,Count-Min Sketch,Null 值的個數,平均長度,不一樣值的數目等等。下面會簡單介紹一下直方圖和 Count-Min Sketch。sql
1. 直方圖簡介數據庫
直方圖是一種對數據分佈狀況進行描述的工具,它會按照數據的值大小進行分桶,並用一些簡單的數據來描述每一個桶,好比落在桶裏的值的個數。大多數數據庫都會選擇用直方圖來進行區間查詢的估算。根據分桶策略的不一樣,常見的直方圖能夠分爲等深直方圖和等寬直方圖。 在 TiDB 中,咱們選擇了等深直方圖,於 1984 年在 Accurate estimation of the number of tuples satisfying a condition 文獻中提出。相比於等寬直方圖,等深直方圖在最壞狀況下也能夠很好的保證偏差。所謂的等深直方圖,就是落入每一個桶裏的值數量儘可能相等。舉個例子,比方說對於給定的集合 {1.6, 1.9, 1.9, 2.0, 2.4, 2.6, 2.7, 2.7, 2.8, 2.9, 3.4, 3.5},而且生成 4 個桶,那麼最終的等深直方圖就會以下圖所示,包含四個桶 [1.6, 1.9],[2.0, 2.6],[2.7, 2.8],[2.9, 3.5],其桶深均爲 3。數組
2. Count-Min Sketch 簡介數據結構
Count-Min Sketch 是一種能夠處理等值查詢,Join 大小估計等的數據結構,而且能夠提供很強的準確性保證。自 2003 年在文獻 An improved data stream summary: The count-min sketch and its applications 中提出以來,因爲其建立和使用的簡單性得到了普遍的使用。 Count-Min Sketch 維護了一個 d*w 的計數數組,對於每個值,用 d 個獨立的 hash 函數映射到每一行的一列中,並對應修改這 d 個位置的計數值。以下圖所示:app
這樣在查詢一個值出現了多少次的時候,依舊用 d 個 hash 函數找到每一行中被映射到的位置,取這 d 個值的最小值做爲估計值。框架
直方圖和 CM-Sketch 是經常使用的兩種數據概要手段,想了解更多相關技術,能夠參考 《Synopses for Massive Data: Samples,Histograms, Wavelets, Sketches》。
經過上面的描述,咱們知道統計信息主要須要建立和維護的是直方圖和 Count-Min Sketch。 經過執行 analyze 語句,TiDB 會收集上述所須要的信息。在執行 analyze 語句的時候,TiDB 會將 analyze 請求下推到每個 Region 上,而後將每個 Region 的結果合併起來。對於 Count-Min Sketch,其建立和合並都比較簡單,在這裏略去不講。如下主要介紹列和索引的直方圖的建立。
1. 列直方圖的建立
在建立直方圖的時候,須要數據是有序的,而排序的代價每每很高,所以咱們在 TiDB 中實現了抽樣算法,對抽樣以後的數據進行排序,創建直方圖,即會在每個 Region 上進行抽樣,隨後在合併結果的時候再進行抽樣。
在 sample.go 中,咱們實現了蓄水池抽樣算法,用來生成均勻抽樣集合。令樣本集合的容量爲 S,在任一時刻 n,數據流中的元素都以 S/n 的機率被選取到樣本集合中去。若是樣本集合大小超出 S,則從中隨機去除一個樣本。舉個例子,假如樣本池大小爲 S = 100 ,從頭開始掃描全表,當讀到的記錄個數 n < 100 時,會把每一條記錄都加入採樣池,這樣保證了在記錄總數小於採樣池大小時,全部記錄都會被選中。而當掃描到的第 n = 101 條時,用機率 P = S/n = 100⁄101 決定是否把這個新的記錄加入採樣池,若是加入了採樣池,採樣池的總數會超過 S 的限制,這時須要隨機選擇一箇舊的採樣丟掉,保證採樣池大小不會超過限制。
採樣完成後,將全部的數據排序,因爲知道採樣事後總的行數和直方圖的桶數,所以就能夠知道每一個桶的深度。這樣就能夠順序遍歷每一個值 V:
2. 索引直方圖的建立
在創建索引列直方圖的時候,因爲不能事先知道有多少行的數據,也就不能肯定每個桶的深度,不過因爲索引列的數據是已經有序的,因次能夠採用以下算法:在肯定了桶的個數以後,將每一個桶的初始深度設爲 1,用前面列直方圖的建立方法插入數據,這樣若是到某一時刻所需桶的個數超過了當前桶深度,那麼將桶深擴大一倍,將以前的每兩個桶合併爲 1 個,而後繼續插入。
在收集了每個 Region 上分別創建的直方圖後,還須要把每一個 Region 上的直方圖進行合併。對於兩個相鄰 Region 上的直方圖,因爲索引是有序的,所以前一個的上界不會大於後一個的下界。不過爲了保證每一個值只在一個桶裏,咱們還須要先處理一下交界處桶的問題,即若是交界處兩個桶的上界和下界相等,那麼須要先合併這兩個桶。若是直方圖合併以後桶的個數超過了限制,那麼只須要把兩兩相鄰的桶合二爲一。
在 2.0 版本中,TiDB 引入了動態更新機制(2.0 版本默認沒有打開, 2.1-beta 版本中已經默認打開),能夠根據查詢的結果去動態調整統計信息。對於直方圖,須要調整桶高和桶的邊界;對於 CM Sketch,須要調整計數數組,使得估計值和查詢的結果相等。
1. 桶高的更新
在範圍查詢的時候,涉及的桶都有可能對最終的結果貢獻一些偏差。所以,一種更新的方法即是假定全部桶貢獻的偏差都是均勻的,即若是最終估計的結果爲 E,實際的結果爲 R,某一個桶的估計結果爲 b = 桶高 h * 覆蓋比例 r,那麼就能夠將這個桶的桶高調整爲 (b / r) * (R / E) = h * (R / E)。不過若是能夠知道落在每個桶範圍中的實際結果,即可以不去假定全部桶貢獻的偏差都是均勻的。
爲了知道落在每個桶範圍中的實際結果,須要先把查詢的範圍按照直方圖桶的邊界切分紅不相交的部分,這樣在 TiKV 在執行查詢的時候,能夠統計出每個範圍中實際含有的行數目。這樣咱們即可以按照相似於前述的方法調整每個桶,不過這個時候不須要假定每一個桶貢獻的偏差都是均勻的,由於咱們能夠準確知道每個桶貢獻的偏差。
2. 桶邊界的更新
在用直方圖估計的時候,對於那些只被查詢範圍覆蓋了一部分的桶,主要的偏差來自連續平均分佈假設。這樣桶邊界更新的主要目即是使得查詢的邊界能儘可能的落在與桶的邊界不遠的地方。桶邊界的更新主要方法包括分裂和合並。 對於分裂,須要解決的問題是哪些桶須要分裂,分裂成幾個,分裂的邊界在哪裏:
分裂完成後,咱們還要去合併桶。首先分裂得來的桶是不能合併的;除此以外,考慮連續的兩個桶,若是第一個桶佔合併後桶的比例爲 r,那麼令合併後產生的偏差爲 abs(合併前第一個桶的高度 - r * 兩個桶的高度和) / 合併前第一個桶的高度,就只須要去合併偏差最小的那些連續的桶。
3. Count-Min Sketch 的更新
CM Sketch 的更新比較簡單,對於某一個等值查詢的反饋結果 x,其估計值是 y,那麼咱們只須要將這個值涉及到的全部點加上 c = x-y。
在查詢語句中,咱們經常會使用一些過濾條件,而統計信息估算的主要做用就是估計通過這些過濾條件後的數據條數,以便優化器選擇最優的執行計劃。在這篇 文檔 中,介紹到 explain 輸出結果中會包含的一列 count,即預計當前 operator 會輸出的數據條數,即是基於統計信息以及 operator 的執行邏輯估算而來。 在這個部分中,咱們會先從最簡單的單一列上的過濾條件開始,而後考慮如何處理多列的狀況。
1. 範圍查詢
對於某一列上的範圍查詢,TiDB 選擇了經常使用的等深直方圖來進行估算。
在前面介紹等深直方圖時,咱們獲得了一個包含四個桶 [1.6, 1.9],[2.0, 2.6],[2.7, 2.8],[2.9, 3.5],其桶深均爲 3 的直方圖。假設咱們獲得了這樣一個直方圖,而且想知道落在區間 [1.7, 2.8] 範圍內的有多少值。把這個區間對應到直方圖上,能夠看到有兩個桶是被徹底覆蓋的,即桶 [2.0, 2.6] 和桶 [2.7,2.8],所以區間 [2.0, 2.8] 內一共有 6 個值;可是第一個桶只被覆蓋了一部分,那麼問題就變成了已經知道區間 [1.6, 1.9] 範圍內有 3 個值,怎樣估算 [1.7, 1.9] 內有多少個值呢?一個經常使用的方法是假設這個範圍的值是連續且均勻的,那麼咱們就能夠按照查範圍佔桶的比例去估算,也就是 (1.9 - 1.7) / (1.9 - 1.6) * 3 = 2。
不過這裏還有一個問題是估算的時候要去算比例,這對於數值類型很簡單,對於其餘類型,比方說字符串類型怎麼辦呢?一個方法是把字符串映射成數字,而後計算比例,具體能夠參見 statistics/scalar.go。
2. 等值查詢
對於相似查詢等於某個值的這樣的等值查詢,直方圖就捉襟見肘了。通常經常使用的估計方法是假設每一個值出現的次數都相等,這樣就能夠用(總行數/不一樣值的數量)來估計。不過在 TiDB 中,咱們選擇了 Count-Min Sketch 的來進行等值查詢的估算。
因爲 Count-Min Sketch 估計的結果老是不小於實際值,所以在 TiDB 中,咱們選擇了文獻 New estimation algorithms for streaming data: Count-min can do more 中提出了一種 Count-Mean-Min Sketch,其與 Count-Min Sketch 在更新的時候是同樣的,區別在與查詢的時候:對於每一行 i,若 hash 函數映射到了值 j,那麼用 (N - CM[i, j]) / (w-1)
(N 是總共的插入的值數量)做爲其餘值產生的噪音,所以用 CM[i,j] - (N - CM[i, j]) / (w-1)
這一行的估計值,而後用全部行的估計值的中位數做爲最後的估計值。
3. 多列查詢
上面兩個小節介紹了 TiDB 是如何對單列上的查詢條件進行估計的,不過實際的查詢語句中每每包含多個列上的多個查詢條件,所以咱們須要考慮如何處理多列的狀況。在 TiDB 中,selectivity.go 中的 Selectivity
函數實現了這個功能,它是統計信息模塊對優化器提供的最重要的接口。
在處理多列之間的查詢條件的時候,一個常見的作法是認爲不一樣列之間是相互獨立的,所以咱們只須要把不一樣列之間的過濾率乘起來。不過,對於索引上的能夠用來構造索引掃描的範圍的過濾條件,即對於一個 (a, b, c)
這樣的索引,相似 (a = 1 and b = 1 and c < 5)
或者 (a = 1 and b = 1)
這樣的條件,將索引中的值編碼後,就能夠用前面提到的方法進行估算,這樣就不須要假定列之間是相互獨立的。
所以,Selectivity
的一個最重要的任務就是將全部的查詢條件分紅儘可能少的組,使得每一組中的條件均可以用某一列或者某一索引上的統計信息進行估計,這樣咱們就能夠作儘可能少的獨立性假設。
在 Selectivity
中,首先計算了每一列和每個索引能夠覆蓋的過濾條件,並用一個 int64
來當作一個 bitset,將該列能夠覆蓋的過濾條件的位置置爲 1。接下來的任務就是選擇儘可能少的 bitset,來覆蓋儘可能多的過濾條件,在這一步中,咱們使用了貪心算法,即每一次在尚未使用的 bitset 中,選擇一個能夠覆蓋最多還沒有覆蓋的過濾條件。最後一步即是用前面提到的方法對每個列和每個索引上的統計信息進行估計,並用獨立性假設將它們組合起來當作最終的結果。
TiDB 源碼閱讀系列文章: