1. 程式人生 > 其它 >淺析資料庫多表連線:ZNBase 的分散式 join 計算

淺析資料庫多表連線:ZNBase 的分散式 join 計算

Join 是 SQL 中的常用操作。在實際的資料庫應用中,我們經常需要從多個數據表中讀取資料,這時我們就可以使用 SQL 語句中的連線(join),在兩個或多個數據表中查詢資料。

常用 Join 演算法

常用的多表連線演算法主要有三類,分別是 Nested-Loop Join、Hash Join 和 Sort Merge Join。

Nested-Loop Join

Simple Nested-Loop Join 是最簡單粗暴的 Join 演算法 ,即通過雙層迴圈比較資料來獲得結果,但是這種演算法顯然太過於粗魯,如果每個表有 1 萬條資料,那麼對資料比較的次數=1萬 * 1萬 =1億次,很顯然這種查詢效率會非常慢。

在 Simple Nested-Loop Join 演算法的基礎上,延申出了 Index Nested-Loop Join 和 block Nested-Loop Join。前者通過減少內層表資料的匹配次數優化查詢效率;後者則是通過一次性快取外層表的多條資料,以此來減少內層表的掃表次數,從而達到提升效能的目的。

Batched Key Access Join (BKA Join) 可以看作是一個性能優化版的 Index Nested-Loop Join。之所以稱為 Batched,是因為它的實現使用了儲存引擎提供的 MRR(Multi-Range Read) 介面批量進行索引查詢,並通過 PK 排序的方法,將隨機索引回錶轉化為順序回表,一定程度上加速了查索引的磁碟 IO。

Hash Join

兩個表若是元組數目過多,逐個遍歷開銷就很大,Hash Join(雜湊連線)是一種提高連線效率的方法。雜湊連線主要分為兩個階段:建立階段(build phase)和探測階段(probe phase)。

在建立階段,首先選擇一個表(一般情況下是較小的那個表,以減少建立雜湊表的時間和空間),對其中每個元組上的連線屬性(join attribute)採用雜湊函式得到雜湊值,從而建立一個雜湊表。

在探測階段,對另一個表,掃描它的每一行並計算連線屬性的雜湊值,與 bulid phase 建立的雜湊表對比,若有落在同一個 bucket 的,如果滿足連線謂詞(predicate)則連線成新的表。

在記憶體足夠大的情況下,建立雜湊表的整個過程都在記憶體中完成,完成連線操作後才放到磁盤裡。因此這個過程也會帶來很多的記憶體消耗。

Merge Join

Merge join 第一個步驟是確保兩個關聯表都是按照關聯的欄位進行排序。如果關聯欄位有可用的索引,並且排序一致,則可以直接進行 merge join 操作;否則需要先對關聯的表按照關聯欄位進行一次排序(就是說在 merge join 前的兩個輸入上,可能都需要執行一個排序操作,再進行 merge join)。

兩個表都按照關聯欄位排序好之後,merge join 操作從每個表取一條記錄開始匹配,如果符合關聯條件,則放入結果集中;否則,將關聯欄位值較小的記錄拋棄,從這條記錄對應的表中取下一條記錄繼續進行匹配,直到整個迴圈結束。

Merge join 操作本身是非常快的,但是 merge join 前進行的排序可能會帶來較大的效能損耗。

ZNBase 採用的分散式 join 運算元

ZNBase 是由浪潮開源的一款分散式 NewSQL 資料庫,其採用的 Join 演算法包括 Merge join、Hash join 和 Lookup join 。

Merge join

在兩個表索引排序相同的情況下,Merge joins 比 Hash joins 在計算和記憶體方面更高效,效能更好。Merge joins 要求在相等列上索引兩個表,並且索引必須具有相同的順序。如果不滿足這些條件,ZNBase 才會轉向較慢的 Hash joins。

Merge joins 在兩個表的索引列上執行,如下所示:

  1. ZNBase 檢查相等列上的索引,並且它們的排序方式相同(即 ASC 或 DESC)。
  2. ZNBase 從每個表中取一行並進行比較。
    • 對於內連線:
      • 如果行相等,則 ZNBase 返回行。
      • 如果有多個匹配項,則返回匹配項的笛卡爾積。
      • 如果行不相等,ZNBase 將丟棄較低值的行並使用下一行重複該過程,直到處理完所有行。
    • 對於外連線:
      • 如果行相等,則 ZNBase 返回行。
      • 如果有多個匹配項,則返回匹配項的笛卡爾積。
      • 如果行不相等,則 ZNBase 將返回 NULL 非匹配列,並使用下一行重複該過程,直到處理完所有行。

HashJoin

如果無法使用一個 Merge join,ZNBase 將使用一個 Hash join。Hash joins 的計算量很大,需要額外的記憶體。

Hash joins 在兩個表上執行,如下所示:

  1. ZNBase 讀取兩個表並嘗試選擇較小的表。
  2. ZNBase 在較小的表上建立記憶體中的雜湊表。如果雜湊表太大,它將溢位到磁碟儲存(這可能會影響效能)。
  3. 然後,ZNBase 掃描大表,查詢雜湊表中的每一行。

Lookup Join

對於普通的 join 演算法,我們注意到,沒有必要對於 Outer 表中每行資料,都對 Inner 表進行一次全表掃操作,很多時候可以通過索引減少資料讀取的代價,這就用到了 Lookup join。

Lookup join 的適配前提是,在 join 的兩個表中,Outer 表上的對應索引列存在索引。在執行過程中,首先讀取小表的資料,然後去大表的索引中找到大概的 scan 範圍,拿大表的資料與小表的資料比較,推進大表最後就可以得出結果。其執行過程簡述如下:

  1. 從 Inner 表中取一批資料;
  2. 通過 join key 以及這一批資料構造在 outer 表的取值範圍,只讀取對應範圍內的資料
  3. 對從 inner 表取出的每一行資料,都與 2 中取出的對應範圍內的每一條資料執行 join 操作並輸出結果交給上層處理
  4. 重複步驟 1.2.3 直到遍歷完 Outer 表為止。

Lookup Join 在執行時會不斷變更狀態,在不同階段進入不同的狀態做 join 處理:

階段一: jrReadingInput 階段

這個階段讀取小表的一塊塊資料,並對每一行資料開始構建對於大表的 index scan 的範圍(命名為 span),構建完成後進入下一個階段。當小表的這一塊資料被讀完後會回到這個狀態繼續讀取,重複直到小表被讀完。

階段二: jrPerformingLookup 階段

這個階段通過階段一得到的 span,將這個 span 中的資料取出放在一個容器中,讓小表讀出的一塊資料每一行去這個容器中的每一行資料做 lookup 查詢,執行 join 操作並將結果儲存在容器中。當資料匹配完成後進入下一階段。

階段三: jrEmittingRows 階段

從階段二中的容器中取出 join 結果輸出到上層。

分散式 join 計算和資料重分佈

與傳統資料庫相比,分散式資料庫的架構有很大的不同。以 ZNBase 為例,資料庫架構可以分為 SQL 層和儲存層,SQL 層的計算節點需要計算資料所在的分片,然後從多個儲存節點拉取所需的資料。

目前 ZNBase 採用兩種辦法實現分散式計算時表的關聯:

重分佈

將兩表按 join 的列,按 hash 特徵重新分佈到每個節點上。執行分散式的 join 時,如果各個執行節點的資料沒有按照 join 列的特徵進行分佈,這個時候就會將資料進行 hash 重分佈,具體操作如下:

1)選取一個 hash 函式對該行資料進行 join 列的 hash 值計算

2)對參與計算的節點數取餘

根據取餘結果將特定行資料分發至對應計算節點進行 join 計算。

廣播

將資料量較小的表進行廣播。

相關的代價計算為:

M + N > min(M,N) * L:廣播;

M + N <= min(M,N) * L:重分佈。

M 和 N 分別為左右表的行數,L 為參與計算的節點個數。

總結

本文介紹了常用的多表連線 Join 演算法,以及分散式資料庫 ZNBase 採用的 Join 演算法和分散式 Join 策略。對相關技術或產品有任何問題歡迎提 issue 或在社群中留言討論。同時歡迎廣大對分散式資料庫感興趣的開發者共同參與 ZNBase 專案的建設。

關於 ZNBase 的更多詳情可以檢視: 官方程式碼倉庫:https://gitee.com/ZNBase/zn-kvs ZNBase 官網:http://www.znbase.com/ 聯絡郵箱:[email protected] 公眾號怎麼推廣