Join背景介紹
SQL的所有操作,可以分為簡單操作(如過濾where、限制次數limit等)和聚合操作(groupBy,join等)。
其中,join操作是最復雜、代價最大的操作類型,是大部分業務場景的性能瓶頸所在;所以,今天我們基于SparkSQL,來簡要的聊一下SparkSQL所支持的幾種常見的Join算法以及其適用場景。
首先,我們需要知道數倉中表格的分類:按照是否會經常涉及到Join操作,可以簡單分為低層次表和高層次表。
低層次表:直接導入數倉的表,列數少,與其他表存在外鍵依賴,查詢起來經常會用到大量Join算法,查詢效率較低
高層次表:由低層次表加工而來,使用SQL將需要join的表預先合并,形成“寬表”。寬表上查詢不需要大量Join,因此效率較高。但是,相對的是,寬表的數據存在大量冗余,同時生成滯后,查詢不及時。
Join使用的結論
Join常見分類&實現機制
當前SparkSQL支持三種Join算法-shuffle hash join、broadcast hash join以及sort merge join。其中前兩者歸根到底都屬于hash join,只不過在hash join之前需要先shuffle還是先broadcast。所以,首先我們來看一下內核hash join的機制。
Hash Join
先來看一個簡單的SQL:select * from order,item where?item.id?= order.id
參與join的兩張表是item和order,join key分別是item.id以及order.id,假設這個Join采用的是hash join算法,整個過程會經歷三步:
1. 確定Build Table(映射表、小表)以及Probe Table(探查表、大表)。其中Build Table用于構建Hash Table,而Probe會遍歷自身所有key,映射到所生成的Hash Table上去匹配。
2. Build Table構建Hash Table。依次讀取Build Table(item)的數據,對于每一行數據根據join key(item.id)進行hash,hash到對應的Bucket,生成hash table中的一條記錄。數據緩存在內存中,如果內存放不下需要dump到外存。
3. Probe Table探測。依次掃描Probe Table(order)的數據,使用相同的hash函數映射Hash Table中的記錄,映射成功之后再檢查join條件(item.id= order.i_id),如果匹配成功就可以將兩者join在一起。
兩點補充:
1 hash join的性能。從上面的原理圖可以看出,hash join對兩張表基本只掃描一次,算法效率是o(a+b),比起蠻力的笛卡爾積算法的a*b快了很多數量級。
2 為什么說Build Table要盡量選擇小表呢?從原理上也看到了,構建的Hash Table是需要被頻繁訪問的,所以Hash Table最好能全部加載到內存里,這也決定了hash join只適合至少一個小表join的場景。
看完了hash join的內核,我們來看一下這種單機的算法,在大數據分布式情況下,應該如何去做。目前成熟的有兩套算法:broadcast hash join和shuffler hash join。
Broadcast Hash Join
broadcast hash join是將其中一張小表廣播分發到另一張大表所在的分區節點上,分別并發地與其上的分區記錄進行hash join。broadcast適用于小表很小,可以直接廣播的場景。
在執行上,主要可以分為以下兩步:
1. broadcast階段:將小表廣播分發到大表所在的所有主機。分發方式可以有driver分發,或者采用p2p方式。
2. hash join階段:在每個executor上執行單機版hash join,小表映射,大表試探;
需要注意的是,Spark中對于可以廣播的小表,默認限制是10M以下。(參數是spark.sql.autoBroadcastJoinThreshold)
Shuffle Hash Join
當join的一張表很小的時候,使用broadcast hash join,無疑效率最高。但是隨著小表逐漸變大,廣播所需內存、帶寬等資源必然就會太大,所以才會有默認10M的資源限制。
所以,當小表逐漸變大時,就需要采用另一種Hash Join來處理:Shuffle Hash Join。
Shuffle Hash Join按照join key進行分區,根據key相同必然分區相同的原理,將大表join分而治之,劃分為小表的join,充分利用集群資源并行化執行。
在執行上,主要可以分為以下兩步:
1. shuffle階段:分別將兩個表按照join key進行分區,將相同join key的記錄重分布到同一節點,兩張表的數據會被重分布到集群中所有節點。
2. hash join階段:每個分區節點上的數據單獨執行單機hash join算法。
剛才也說過,Hash Join適合至少有一個小表的情況,那如果兩個大表需要Join呢?這時候就需要Sort-Merge Join了。
Sort-Merge Join
SparkSQL對兩張大表join采用了全新的算法-sort-merge join,整個過程分為三個步驟:
1. shuffle階段:將兩張大表根據join key進行重新分區,兩張表數據會分布到整個集群,以便分布式并行處理
2. sort階段:對單個分區節點的兩表數據,分別進行排序
3. merge階段:對排好序的兩張分區表數據執行join操作。join操作很簡單,分別遍歷兩個有序序列,碰到相同join key就merge輸出,否則繼續取更小一邊的key。
仔細分析的話會發現,sort-merge join的代價并不比shuffle hash join小,反而是多了很多。那為什么SparkSQL還會在兩張大表的場景下選擇使用sort-merge join算法呢?
這和Spark的shuffle實現有關,目前spark的shuffle實現都適用sort-based shuffle算法,因此在經過shuffle之后partition數據都是按照key排序的。因此理論上可以認為數據經過shuffle之后是不需要sort的,可以直接merge。
結論:如何優化
經過上文的分析,可以明確每種Join算法都有自己的適用場景。在優化的時候,除了要根據業務場景選擇合適的join算法之外,還要注意以下幾點:
1 數據倉庫設計時最好避免大表與大表的join查詢。
2 SparkSQL也可以根據內存資源、帶寬資源適量將參數spark.sql.autoBroadcastJoinThreshold調大,讓更多join實際執行為broadcast hash join。
文集
文章
參考鏈接:
SparkSQL – 有必要坐下來聊聊Join:http://hbasefly.com/2017/03/19/sparksql-basic-join/