TiDB 源碼閱讀系列文章(十五)Sort Merge Join

什麼是 Sort Merge Join

在開始閱讀源碼以前, 咱們來看看什麼是 Sort Merge Join (SMJ),定義能夠看 wikipedia。簡單說來就是將 Join 的兩個表,首先根據鏈接屬性進行排序,而後進行一次掃描歸併, 進而就能夠得出最後的結果。這個算法最大的消耗在於對內外表數據進行排序,而當鏈接列爲索引列時,咱們能夠利用索引的有序性避免排序帶來的消耗, 因此一般在查詢優化器中,鏈接列爲索引列的狀況下能夠考慮選擇使用 SMJ。git

TiDB Sort Merge Join 實現

執行過程

TiDB 的實現代碼在 tidb/executor/merge_join.goMergeJoinExec.NextChunk 是這個算子的入口。下面以 SELECT * FROM A JOIN B ON A.a = B.a 爲例,對 SMJ 執行過程進行簡述,假設此時外表爲 A,內表爲 B,join-keys 爲 a,A,B 表的 a 列上都有索引:github

  1. 順序讀取外表 A 直到 join-keys 中出現另外的值,把相同 keys 的行放入數組 a1,一樣的規則讀取內表 B,把相同 keys 的行放入數組 a2。若是外表數據或者內表數據讀取結束,退出。算法

  2. 從 a1 中讀取當前第一行數據,設爲 v1。從 a2 中讀取當前第一行數據,設爲 v2。express

  3. 根據 join-keys 比較 v1,v2,結果分爲幾種狀況:數組

    • cmpResult > 0, 表示 v1 大於 v2,把當前 a2 的數據丟棄,從內表讀取下一批數據,讀取方法同 1。重複 2。
    • cmpResult < 0, 表示 v1 小於 v2,說明外表的 v1 沒有內表的值與之相同,把外表數據輸出給 resultGenerator(不一樣的鏈接類型會有不一樣的結果輸出,例如外鏈接會把不匹配的外表數據輸出)。
    • cmpResult == 0, 表示 v1 等於 v2。那麼遍歷 a1 裏面的數據,跟 a2 的數據,輸出給 resultGenerator 做一次鏈接。
  4. 回到步驟 1。框架

下面的圖展現了 SMJ 的過程:函數

圖 1 SMJ 過程.png

讀取內表 / 外表數據

咱們分別經過 fetchNextInnerRows 或者 fetchNextOuterRows 讀取內表和外表的數據。這兩個函數實現的功能相似,這裏只詳述函數 fetchNextInnerRows 的實現。源碼分析

MergeSortExec 算子讀取數據,是經過迭代器 readerIterator 完成,readerIterator 能夠順序讀取數據。MergeSortExec 算子維護兩個 readerIterator:outerIterinnerIter,它們在 buildMergeJoin 函數中被構造。fetch

真正讀取數據的操做是在 readerIterator.nextSelectedRow 中完成, 這裏會經過 ri.reader.NextChunk 每次讀取一個 Chunk 的數據,關於 Chunk 的相關內容,能夠查看咱們以前的文章 TiDB 源碼閱讀系列文章(十)Chunk 和執行框架簡介優化

這裏值得注意的是,咱們經過 expression.VectorizedFilter 對外表數據進行過濾,返回一個 curSelected 布爾數組,用於外表的每一行數據是不是知足 filter 過濾條件。以 select * from t1 left outer join t2 on t1.a=100; 爲例, 這裏的 filter 是 t1.a=100, 對於沒有經過這個過濾條件的行,咱們經過 ri.joinResultGenerator.emitToChunk 函數發送給 resultGenerator, 這個 resultGenerator 是一個 interface,具體是否輸出這行數據,會由 join 的類型決定,好比外鏈接則會輸出,內鏈接則會忽略。具體關於 resultGenerator, 能夠參考以前的文章:TiDB 源碼閱讀系列文章(九)Hash Join

rowsWithSameKey 經過 nextSelectedRow 不斷讀取下一行數據,並經過對每行數據的 join-keys 進行判斷是否是屬於同一個 join-keys,若是是,會把相同 join-keys 的行分別放入到 innerChunkRowsouterIter4Row 數組中。而後對其分別創建迭代器 innerIter4Row 和 outerIter4Row。在 SMJ 中的執行過程當中,會利用這兩個迭代器來獲取數據進行真正的比較得出 join result。

Merge-Join

實現 Merge-Join 邏輯的代碼在函數 MergeJoinExec.joinToChunk, 對內外表迭代器的當前數據根據各自的 join-keys 做對比,有以下幾個結果:

  • cmpResult > 0,表明外表當前數據大於內表數據,那麼經過 fetchNextInnerRows 直接讀取下一個內表數據,而後從新比較便可。

  • cmpResult < 0,表明外表當前數據小於內表數據,這個時候就分幾種狀況了,若是是外鏈接,那麼須要輸出外表數據 + NULL,若是是內鏈接,那麼這個外表數據就被忽略,對於這個不一樣邏輯的處理,統一由 e.resultGenerator 來控制,咱們只須要把外表數據經過 e.resultGenerator.emitToChunk 調用它便可。而後經過 fetchNextOuterRows 讀取下一個外表數據,從新比較。

  • cmpResult == 0,表明外表當前數據等於內表當前數據,這個時候就把外表數據跟內表當前數據作一次鏈接,經過 e.resultGenerator.emitToChunk 生成結果。以後外表跟內表分別獲取下一個數據,從新開始比較。

重複上面的過程,直到外表或者內表數據被遍歷完,退出 Merge-Join 的過程。

更多

咱們上面的分析代碼基於 Source-code 分支,可能你們已經發現了一些問題,好比咱們會一次性讀取內外表的 Join group(相同的 key)。這裏若是相同的 key 比較多,是有內存 OOM 的風險的。針對這個問題,咱們在最新的 master 分支作了幾個事情來優化:

  1. 外表其實不須要把相同的 keys 一次性都讀取上來, 它只須要按次迭代外表數據,再跟內表逐一對比做鏈接便可。這裏至少能夠減小外表發生 OOM 的問題,能夠大大減小 OOM 的機率。

  2. 對於內表,咱們對 OOM 也不是沒有辦法,咱們用 memory.Tracker 這個內存追蹤器來記錄當前內表已經使用的中間結果的內存大小,若是它超過咱們設置的閾值,咱們會採起輸出日誌或者終止 SQL 繼續運行的方法來規避 OOM 的發生。關於 memory.Tracker 咱們不在此展開,能夠留意咱們後續的源碼分析文章。

後續咱們還會在 Merge-Join 方面作一些優化, 好比咱們能夠作多路歸併,中間結果存外存等等,敬請期待。

做者:姚維

相關文章
相關標籤/搜索