【SparkSQL】聊一聊 Join

1. Join 背景介紹

Join 是數據庫查詢永遠繞不開的話題,傳統查詢 SQL 技術總體可以分爲簡單操作(過濾操作 WHERE、排序操作 LIMIT 等),聚合操作 GROUPBY 等以及 JOIN 操作等。其中 Join 操作是其中最複雜、代價最大的操作類型,也是 OLAP 場景中使用相對較多的操作。因此很有必要聊聊這個話題。

另外,從業務層面來講,用戶在數倉建設的時候也會涉及 Join 使用的問題。通常情況下,數據倉庫中的表一般會分爲“低層次表”和“高層次表”。

所謂“低層次表”,就是數據源導入數倉之後直接生成的表,單表列值較少,一般可以明顯歸爲維度表或者事實表,表和表之間大多存在外健依賴,所以查詢起來會遇到大量 Join 運算,查詢效率相對比較差。而“高層次表”是在”低層次表”的基礎上加工轉換而來,通常做法是使用 SQL 語句將需要 Join 的表預先進行合併形成“寬表”,在寬表上的查詢因爲不需要執行大量 Join 因而效率相對較高,很明顯,寬表缺點是數據會有大量冗餘,而且生成相對比較滯後,查詢結果可能並不及時。

因此,爲了獲得實效性更高的查詢結果,大多數場景還是需要進行復雜的 Join 操作。Join 操作之所以複雜,不僅僅因爲通常情況下其時間空間複雜度高,更重要的是它有很多算法,在不同場景下需要選擇特定算法才能獲得最好的優化效果。關係型數據庫也有關於 Join 的各種用法,姜承堯大神之前由淺入深地介紹過 MySQL Join 的各種算法以及調優方案。本文接下來會介紹 SparkSQL 所支持的幾種常見的 Join 算法以及其適用場景。

2. Join 常見分類以及基本實現機制

當前 SparkSQL 支持三種 Join 算法

  1. Shuffle Hash Join
  2. Broadcast Hash Join
  3. Sort Merge Join

其中前兩者歸根到底都屬於 Hash Join,只不過在 Hash Join 之前需要先 Shuffle 還是先 Broadcast。其實,這些算法並不是什麼新鮮玩意,都是數據庫幾十年前的老古董了(參考),只不過換上了分佈式的皮而已。不過話說回來,SparkSQL/Hive…等等,所有這些大數據技術哪一樣不是來自於傳統數據庫技術,什麼語法解析 AST、基於規則優化(CRO)、基於代價優化(CBO)、列存,都來自於傳統數據庫。就拿 Shuffle Hash JoinBroadcast Hash Join 來說,Hash Join 算法就來自於傳統數據庫,而 Shuffle 和 Broadcast 是大數據的皮,兩者一結合就成了大數據的算法了。因此可以這樣說,大數據的根就是傳統數據庫,傳統數據庫人才可以很快的轉型到大數據。好吧,這些都是閒篇。

繼續來看技術,既然 Hash Join 是“內核”,那就刨出來看看,看完把“皮”再分析一下。

2.1 Hash Join

先來看看這樣一條SQL語句:select * from order,item where item.id = order.i_id,很簡單一個 Join 節點,參與 Join 的兩張表是 item 和 order,Join Key 分別是 item.id 以及 order.i_id。現在假設這個 Join 採用的是 Hash Join 算法,整個過程會經歷三步:

  1. 確定 Build Table 以及 Probe Table:這個概念比較重要,Build Table 使用 Join Key 構建 Hash Table,而 Probe Table 使用 Join Key 進行探測,探測成功就可以 Join 在一起。通常情況下,小表會作爲 Build Table,大表作爲 Probe Table。此事例中 item 爲Build Table,order 爲 Probe Table。
  2. 構建 Hash Table:依次讀取 Build Table(item)的數據,對於每一行數據根據 Join Key(item.id)進行 Hash,Hash 到對應的 Bucket,生成 Hash Table 中的一條記錄。數據緩存在內存中,如果內存放不下需要 dump 到外存。
  3. 探測:再依次掃描 Probe Table(order)的數據,使用相同的 Hash 函數映射 Hash Table 中的記錄,映射成功之後再檢查 Join 條件(item.id = order.i_id),如果匹配成功就可以將兩者 Join 在一起。
    Hash Join

基本流程可以參考上圖,這裏有兩個小問題需要關注:

  1. Hash Join 性能如何?很顯然,hash join 基本都只掃描兩表一次,可以認爲 O(a+b),較之最極端的笛卡爾集運算 O(a*b),不知甩了多少條街;
  2. 爲什麼 Build Table 選擇小表?道理很簡單,因爲構建的 Hash Table 最好能全部加載在內存,效率最高;這也決定了 Hash Join 算法只適合至少一個小表的 Join 場景,對於兩個大表的 Join 場景並不適用;

上文說過,Hash Join 是傳統數據庫中的單機 Join 算法,在分佈式環境下需要經過一定的分佈式改造,說到底就是儘可能利用分佈式計算資源進行並行化計算,提高總體效率。Hash Join 分佈式改造一般有兩種經典方案:

  1. Broadcast Hash Join:將其中一張小表廣播分發到另一張大表所在的分區節點上,分別併發地與其上的分區記錄進行 Hash Join。Broadcast 適用於小表很小,可以直接廣播的場景。
  2. Shuffler Hash Join:一旦小表數據量較大,此時就不再適合進行廣播分發。這種情況下,可以根據 Join Key 相同必然分區相同的原理,將兩張表分別按照 Join Key 進行重新組織分區,這樣就可以將 Join 分而治之,劃分爲很多小 Join,充分利用集羣資源並行化。

2.1.1 Broadcast Hash Join

如下圖所示,Broadcast Hash Join 可以分爲兩步:

  1. Broadcast 階段:將小表廣播分發到大表所在的所有主機。廣播算法可以有很多,最簡單的是先發給 Driver,Driver 再統一分發給所有 Executor;要不就是基於 Bittorrete 的 P2P 思路;
  2. Hash Join 階段:在每個 Executor 上執行單機版 Hash Join,小表映射,大表試探;
    Broadcast Hash Join
    SparkSQL 規定 Broadcast Hash Join 執行的基本條件爲被廣播小表必須小於參數 spark.sql.autoBroadcastJoinThreshold,默認爲10M。

2.1.2 Shuffle Hash Join

在大數據條件下如果一張表很小,執行 join 操作最優的選擇無疑是Broadcast Hash Join,效率最高。但是一旦小表數據量增大,廣播所需內存、帶寬等資源必然就會太大,Broadcast Hash Join 就不再是最優方案。此時可以按照 Join Key 進行分區,根據 Key 相同必然分區相同的原理,就可以將大表 Join 分而治之,劃分爲很多小表的 Join,充分利用集羣資源並行化。如下圖所示,Shuffle Hash Join 也可以分爲兩步:

  1. Shuffle 階段:分別將兩個表按照 Join Key 進行分區,將相同 Join Key 的記錄重分佈到同一節點,兩張表的數據會被重分佈到集羣中所有節點。這個過程稱爲 Shuffle;
  2. Hash Join 階段:每個分區節點上的數據單獨執行單機 Hash Join 算法。
    Shuffle Hash Join
    看到這裏,可以初步總結出來如果兩張小表 Join 可以直接使用單機版 Hash Join;如果一張大表 Join 一張極小表,可以選擇 Broadcast Hash Join 算法;而如果是一張大表 Join 一張小表,則可以選擇 Shuffle Hash Join 算法;那如果是兩張大表進行 Join 呢?

2.2 Sort Merge Join

SparkSQL 對兩張大表 Join 採用了全新的算法 Sort Merge Join,如下圖所示,整個過程分爲三個步驟:

  1. Shuffle 階段:將兩張大表根據 Join Key 進行重新分區,兩張表數據會分佈到整個集羣,以便分佈式並行處理;
  2. Sort 階段:對單個分區節點的兩表數據,分別進行排序;
  3. Merge 階段:對排好序的兩張分區表數據執行 Join 操作。
    Sort Merge Join
    Join 操作很簡單,分別遍歷兩個有序序列,碰到相同 Join Key 就 Merge 輸出,否則取更小一邊,見下圖示意:
    Sort Merge Join的 Join 過程

仔細分析的話會發現,Sort Merge Join 的代價並不比 Shuffle Hash Join 小,反而是多了很多。那爲什麼 SparkSQL 還會在兩張大表的場景下選擇使用 Sort Merge Join 算法呢?這和 Spark 的 Shuffle 實現有關,目前 Spark 的 Shuffle 實現都適用 Sort Merge Join 算法,因此在經過 Shuffle 之後 Partition 數據都是按照 Key 排序的。因此理論上可以認爲數據經過 Shuffle 之後是不需要 Sort 的,可以直接 Merge。

經過上文的分析,可以明確每種 Join 算法都有自己的適用場景,數據倉庫設計時最好避免大表與大表的 Join 查詢,SparkSQL 也可以根據內存資源、帶寬資源適量將參數 spark.sql.autoBroadcastJoinThreshold 調大,讓更多 Join 實際執行爲 Broadcast Hash Join

3. 總結

Join 操作是傳統數據庫中的一個高級特性,尤其對於當前 MySQL 數據庫更是如此,原因很簡單,MySQL 對 Join 的支持目前還比較有限,只支持 Nested Loop Join 算法,因此在 OLAP 場景下 MySQL 是很難喫的消的,不要去用 MySQL 去跑任何 OLAP 業務,結果真的很難看。不過好消息是 MySQL 在新版本要開始支持 Hash Join 了,這樣也許在將來也可以用 MySQL 來處理一些小規模的 OLAP 業務。

和 MySQL 相比,PostgreSQL、SQLServer、Oracle 等這些數據庫對 Join 支持更加全面一些,都支持 Hash Join 算法。由 PostgreSQL 作爲內核構建的分佈式系統 Greenplum 更是在數據倉庫中佔有一席之地,這和 PostgreSQL 對 Join 算法的支持其實有很大關係。

總體而言,傳統數據庫單機模式做 Join 的場景畢竟有限,也建議儘量減少使用 Join。然而大數據領域就完全不同,Join 是標配, OLAP 業務根本無法離開表與表之間的關聯,對 Join 的支持成熟度一定程度上決定了系統的性能,誇張點說,“得 Join 者得天下”。本文只是試圖帶大家真正走進 Join 的世界,瞭解常用的幾種 Join 算法以及各自的適用場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章