Hive中小表與大表關聯 join 的效能分析

2021-08-18 03:45:06 字數 2760 閱讀 7880

經常看到一些hive優化的建議中說當小表與大表做關聯時,把小表寫在前面,這樣可以使hive的關聯速度更快,提到的原因都是說因為小表可以先放到記憶體中,然後大表的每條記錄再去記憶體中檢測,最終完成關聯查詢。這樣的原因看似合理,但是仔細推敲,又站不住腳跟。 

多小的表算小表?如果所謂的小表在記憶體中放不下怎麼辦?我用2個只有幾條記錄的表做關聯查詢,這應該算是小表了,在檢視reduce的執行日誌時依然是有寫磁碟的操作的。實際上reduce在接收全部map的輸出後一定會有乙個排序所有鍵值對並合併寫入磁碟檔案的操作。寫入磁碟(spill)有可能是多次的,因此有可能會生成多個臨時檔案,但是最終都要合併成乙個檔案,即最終每乙個reduce都只處理乙個檔案。 

我做了乙個實驗,用1條記錄的表和3億多條記錄的表做join,無論小表是放在join的前面還是join的後面,執行的時間幾乎都是相同的。再去看reduce的執行日誌,1條記錄的表在join前或者join後兩次查詢的reduce日誌幾乎也是一摸一樣的。如果按照上面的說法把join左側的表放記憶體等待join右側的表到記憶體中去檢測,那麼當3億多條記錄的表放在join左側時,記憶體肯定是無法容下這麼多記錄的,勢必要進行寫磁碟的操作,那它的執行時間應該會比小表在join前時長很多才對,但事實並不是這樣,也就說明了上面說到的原因並不合理。 

事實上「把小表放在前面做關聯可以提高效率」這種說法是錯誤的。正確的說法應該是「把重複關聯鍵少的表放在join前面做關聯可以提高join的效率。」

分析一下hive對於兩表關聯在底層是如何實現的。因為不論多複雜的hive查詢,最終都要轉化成mapreduce的job去執行,因此hive對於關聯的實現應該和mapreduce對於關聯的實現類似。而mapreduce對於關聯的實現,簡單來說,是把關聯鍵和標記是在join左邊還是右邊的標識位作為組合鍵(key),把一條記錄以及標記是在join左邊還是右邊的標識位組合起來作為值(value)。在reduce的shuffle階段,按照組合鍵的關聯鍵進行主排序,當關聯鍵相同時,再按照標識位進行輔助排序。而在分割槽段時,只用關聯鍵中的關聯鍵進行分割槽段,這樣關聯鍵相同的記錄就會放在同乙個value list中,同時保證了join左邊的表的記錄在value list的前面,而join右邊的表的記錄在value list的後面。 

例如a join b on (a.id = b.id) ,假設a表和b表都有1條id = 3的記錄,那麼a表這條記錄的組合鍵是(3,0),b表這條記錄的組合鍵是(3,1)。排序時可以保證a表的記錄在b表的記錄的前面。而在reduce做處理時,把id=3的放在同乙個value list中,形成 key = 3,value list = [a表id=3的記錄,b表id=3的記錄] 

接下來我們再來看當兩個表做關聯時reduce做了什麼。reduce會一起處理id相同的所有記錄。我們把value list用陣列來表示。 

1)   reduce先讀取第一條記錄v[0],如果發現v[0]是b表的記錄,那說明沒有a表的記錄,最終不會關聯輸出,因此不用再繼續處理這個id了,讀取v[0]用了1次讀取操作。 

2)   如果發現v[0]到v[length-1]全部是a表的記錄,那說明沒有b表的記錄,同樣最終不會關聯輸出,但是這裡注意,已經對value做了length次的讀取操作。 

3)   例如a表id=3有1條記錄,b表id=3有10條記錄。首先讀取v[0]發現是a表的記錄,用了1次讀取操作。然後再讀取v[1]發現是b表的操作,這時v[0]和v[1]可以直接關聯輸出了,累計用了2次操作。這時候reduce已經知道從v[1]開始後面都是b 表的記錄了,因此可以直接用v[0]依次和v[2],v[3]……v[10]做關聯操作並輸出,累計用了11次操作。 

4)   換過來,假設a表id=3有10條記錄,b表id=3有1條記錄。首先讀取v[0]發現是a表的記錄,用了1次讀取操作。然後再讀取v[1]發現依然是a表的記錄,累計用了2次讀取操作。以此類推,讀取v[9]時發現還是a表的記錄,累計用了10次讀取操作。然後讀取最後1條記錄v[10]發現是b表的記錄,可以將v[0]和v[10]進行關聯輸出,累計用了11次操作。接下來可以直接把v[1]~v[9]分別與v[10]進行關聯輸出,累計用了20次操作。 

5)   再複雜一點,假設a表id=3有2條記錄,b表id=3有5條記錄。首先讀取v[0]發現是a表的記錄,用了1次讀取操作。然後再讀取v[1]發現依然是a表的記錄,累計用了2次讀取操作。然後讀取v[2]發現是b表的記錄,此時v[0]和v[2]可以直接關聯輸出,累計用了3次操作。接下來v[0]可以依次和v[3]~v[6]進行關聯輸出,累計用了7次操作。接下來v[1]再依次和v[2]~v[6]進行關聯輸出,累計用了12次操作。 

6)   把5的例子調過來,假設a表id=3有5條記錄,b表id=3有2條記錄。先讀取v[0]發現是a表的記錄,用了1次讀取操作。然後再讀取v[1]發現依然是a表的記錄,累計用了2次讀取操作。以此類推,讀取到v[4]發現依然是a表的記錄,累計用了5次讀取操作。接下來讀取v[5],發現是b表的記錄,此時v[0]和v[5]可以直接關聯輸出,累計用了6次操作。然後v[0]和v[6]進行關聯輸出,累計用了7次操作。然後v[1]分別與v[5]、v[6]關聯輸出,累計用了9次操作。v[2] 分別與v[5]、v[6]關聯輸出,累計用了11次操作。以此類推,最後v[4] 分別與v[5]、v[6]關聯輸出,累計用了15次操作。 

7)   額外提一下,當reduce檢測a表的記錄時,還要記錄a表同乙個key的記錄的條數,當發現同乙個key的記錄個數超過hive.skewjoin.key的值(預設為1000000)時,會在reduce的日誌中列印出該key,並標記為傾斜的關聯鍵。 

最終得出的結論是:寫在關聯左側的表每有1條重複的關聯鍵時底層就會多1次運算處理。

假設a表有一千萬個id,平均每個id有3條重複值,那麼把a表放在前面做關聯就會多做三千萬次的運算處理,這時候誰寫在前誰寫在後就看出效能的差別來了。 

Hive中小表與大表關聯 join 的效能分析

經常看到一些hive優化的建議中說當小表與大表做關聯時,把小表寫在前面,這樣可以使hive的關聯速度更快,提到的原因都是說因為小表可以先放到記憶體中,然後大表的每條記錄再去記憶體中檢測,最終完成關聯查詢。這樣的原因看似合理,但是仔細推敲,又站不住腳跟。多小的表算小表?如果所謂的小表在記憶體中放不下怎...

Hive中小表與大表關聯 join 的效能分析

經常看到一些hive優化的建議中說當小表與大表做關聯時,把小表寫在前面,這樣可以使hive的關聯速度更快,提到的原因都是說因為小表可以先放到記憶體中,然後大表的每條記錄再去記憶體中檢測,最終完成關聯查詢。這樣的原因看似合理,但是仔細推敲,又站不住腳跟。多小的表算小表?如果所謂的小表在記憶體中放不下怎...

Hive中小表和大表關聯 join 的效能分析

經常看到一些hive優化的建議中說當小表與大表做關聯時,把小表寫在前面,這樣可以使hive的關聯速度更快,提到的原因都是說因為小表可以先放到記憶體中,然後大表的每條記錄再去記憶體中檢測,最終完成關聯查詢。這樣的原因看似合理,但是仔細推敲,又站不住腳跟。多小的表算小表?如果所謂的小表在記憶體中放不下怎...