1. Join 的基本原理
大家都知道,Hive 會將所有的 SQL 查詢轉化爲 Map/Reduce 作業運行於 Hadoop 集羣之上。在這裏簡要介紹 Hive 將 Join 轉化爲 Map/Reduce 的基本原理。
假定有 user 和 order 兩張表,分別如下:
user 表:
sid | name |
---|---|
1 | apple |
2 | orange |
order 表:
uid | orderid |
---|---|
1 | 1001 |
1 | 1002 |
2 | 1003 |
現在想做 student 和 sc 兩張表上的連接操作:
SELECT
u.name,
o.orderid
FROM user u
JOIN order o ON u.uid = o.uid;
Hive 是利用 hadoop 的 Map/Reduce 計算框架執行上述查詢的呢??
Hive 會將出現在連接條件 ON 之後的所有字段作爲 Map 輸出的 key,將所需的字段作爲 value,構建(key, value),同時爲每張表打上不同的標記 tag,輸出到 Reduce 端。在 Reduce 端,根據 tag 區分參與連接的表,實現連接操作。
我們使用下圖來模擬這個過程:
在 Map 端分別掃描 user 和 order 的兩張表。對於 user 表,在連接條件 ON 之後的字段爲 uid,所以以 uid 作爲 Map 輸出的 key,在 SELECT 語句中還需要 name 字段,所以 name 字段作爲 value 的一部分,同時爲 user 表賦予標記 tag=1,這樣處理 user 表的 mapper 地輸出形式爲:(uid, “1” + name)。類似的,處理 order 表的 mapper 地輸出形式爲:(uid, “2” + orderid),注意,order 表的標記爲 2。
具有相同 uid 的地(key, value)字段在 reduce 端“集合”,根據 value 中 tag 字段區分來自不同表的數據,使用兩層循環完成連接操作。
上面就是將 Join 操作轉換爲 Map/Reduce 作業的基本原理: 在 map 端掃描表,在 reduce 端完成連接操作。
2. Hive 中的各種 Join
寫在前面的話:”Hive 不支持非等值連接”
我們使用例子講述各種 Join 的區別。假設 my_user 和 my_order 兩張表的數據變爲:
my_user 表:
uid | name |
---|---|
1 | apple |
2 | orange |
3 | banana |
my_order 表:
uid | orderid |
---|---|
1 | 1001 |
1 | 1002 |
2 | 1003 |
2 | 1003 |
4 | 2001 |
注意,my_order 中有一條重複記錄。
“不要考慮例子在現實中的意義,這裏這是爲了演示各種 JOIN 的區別”
2.1 INNER JOIN
INNER JOIN,又稱“內連接”,執行 INNER JOIN 時,只有兩個表中都有滿足連接條件的記錄時纔會保留數據。執行以下語句:
SELECT
u.name,
o.orderid
FROM my_user u
JOIN my_order o ON u.uid = o.uid;
結果爲:
name | orderid |
---|---|
apple | 1001 |
apple | 1002 |
orange | 1003 |
orange | 1003 |
因爲表 my_order 中又重複記錄,所以結果中也有重複記錄。
2.2 LEFT OUTER JOIN
LEFT OUTER JOIN(左外連接),JOIN 操作符左邊表中符合 WHERE 條件的所有記錄都會被保留,JOIN 操作符右邊表中如果沒有符合 ON 後面連接條件的記錄,則從右邊表中選出的列爲 NULL。
執行以下語句:
SELECT
u.name,
o.orderid
FROM my_user u
LEFT OUTER JOIN my_order o ON u.uid = o.uid;
結果爲:
name | orderid |
---|---|
apple | 1001 |
apple | 1002 |
orange | 1003 |
orange | 1003 |
banana | NULL |
這裏由於沒有 WHERE 條件,所以左邊表 my_user 中的記錄都被保留,對於 uid=3 的記錄,在右邊表 my_order 中沒有相應記錄,所以 orderid 爲 NULL。
2.3 RIGHT OUTER JOIN
RIGHT OUTER JOIN(右外連接),LEFT OUTER JOIN 相對,JOIN 操作符右邊表中符合 WHERE 條件的所有記錄都會被保留,JOIN 操作符左邊表中如果沒有符合 ON 後面連接條件的記錄,則從左邊表中選出的列爲 NULL。
SELECT
u.name,
o.orderid
FROM my_user u
RIGHT OUTER JOIN my_order o ON u.uid = o.uid;
執行上面 SQL 語句的結果爲:
name | orderid |
---|---|
apple | 1001 |
apple | 1002 |
orange | 1003 |
orange | 1003 |
NULL | 2001 |
由於左表 my_user 中不存在 uid=4 的記錄,所以 orderid=2001 的記錄對應的 name 爲 NULL。
2.3 FULL OUTER JOIN
結合上面的 LEFT OUTER JOIN 和 RIGHT OUTER JOIN,很容易想到 FULL OUTER JOIN 的運行機制:保留滿足 WHERE 條件的兩個表的數據,沒有符合連接條件的字段使用 NULL 填充。來看一個例子:
SELECT
u.name,
o.orderid
FROM my_user u
FULL OUTER JOIN my_order o ON u.uid = o.uid;
執行結果爲:
name | orderid |
---|---|
apple | 1001 |
apple | 1002 |
orange | 1003 |
orange | 1003 |
banana | NULL |
NULL | 2001 |
原因不再解釋,請自行思考。
2.4 LEFT SMEI JOIN
在早期的 Hive 版本中,不是 IN 關鍵字,可以使用 LEFT SEMI JOIN 實現類似的功能。
LEFT SEMI JOIN(左半開連接)返回左邊表的記錄,前提是右邊表具有滿足 ON 連接條件的記錄。
先來看一個例子:
SELECT
*
FROM my_user u
LEFT SEMI JOIN my_order o ON u.uid = o.uid;
執行結果爲:
uid | name |
---|---|
1 | apple |
2 | orange |
雖然 SELECT 中使用’*‘,但是隻返回了左表 my_user 的列,而且重複的記錄沒有返回(重複記錄在 my_order 表中)
需要強調的是:
- 在 LEFT SEMI JOIN 中,SELECT 中不允許出現右表中的列
- 對於左表中的一條記錄,在右表表中一旦找到匹配記錄就停止掃描
3. JOIN 優化
現實環境中會進行大量的表連接操作,而且表連接操作通常會耗費很懂時間。因此掌握一些基本的 JOIN 優化方法成爲熟練運用 Hive、提高工作效率的基本手段。下面討論一些常用的 JOIN 優化方法。
3.1 MAP-JOIN
本文一開始介紹了 Hive 中 JOIN 的基本原理,這種 JOIN 沒有數據大小的限制,理論上可以用於任何情形。但缺點是:需要 map 端和 reduce 端兩個階段,而且 JOIN 操作是在 reduce 端完成的,稱爲 reduce side join。
那麼,能否省略 reduce 端,直接在 map 端執行的“map side join”操作呢??答案是,可以的。
但有個條件,就是:連接的表中必須有一個小表足以放到每個 mapper 所在的機器的內存中。
下圖展示了 map side join 的原理。
從上圖中可以看出,每個 mapper 都會拿到小表的一個副本,然後每個 mapper 掃描大表中的一部分數據,與各自的小表副本完成連接操作,這樣就可以在 map 端完成連接操作。
那多大的表纔算是“小表”呢??
默認情況下,25M 以下的表是“小表”,該屬性由hive.smalltable.filesize
決定。
有兩種方法使用 map side join:
- 直接在 SELECT 語句中指定“小表”,語法是/+MAPJOIN (tbl)/,其中 tbl 就是要複製到每個 mapper 中去的小表。例如:
SELECT
/*+ MAPJOIN(my_order)*/
u.name,
o.orderid
FROM my_user u
LEFT OUTER JOIN my_order o ON u.uid = o.uid;
- 設置
hive.auto.convert.join = true
,這樣 hive 會自動判斷當前的 join 操作是否合適做 map join,主要是找 join 的兩個表中有沒有小表。
但 JOIN 的兩個表都不是“小表”的時候該怎麼辦呢??這就需要 BUCKET MAP JOIN 上場了。
3.2 BUCKET MAP JOIN
Map side join 固然得人心,但終會有“小表”條件不滿足的時候。這就需要 bucket map join 了。
Bucket map join 需要待連接的兩個表在連接字段上進行分桶(每個分桶對應 hdfs 上的一個文件),而且小表的桶數需要時大表桶數的倍數。建立分桶表的例子:
CREATE TABLE my_user
(
uid INT,
name STRING
)
CLUSTERED BY (uid) into 32 buckets
STORED AS TEXTFILE;
這樣,my_user 表就對應 32 個桶,數據根據 uid 的 hash value 與 32 取餘,然後被分發導不同的桶中。
如果兩個表在連接字段上分桶,則可以執行 bucket map join 了。具體的:
- 設置屬性
hive.optimize.bucketmapjoin= true
控制 hive 執行 bucket map join; - 對小表的每個分桶文件建立一個 hashtable,並分發到所有做連接的 map 端;
- map 端接受了 N(N 爲小表分桶的個數) 個小表的 hashtable,做連接 操作的時候,只需要將小表的一個 hashtable 放入內存即可,然後將大表的對應的 split 拿出來進行連接,所以其內存限制爲小表中最大的那個 hashtable 的大小
3.3 SORT MERGE BUCKET MAP JOIN
對於 bucket map join 中的兩個表,如果每個桶內分區字段也是有序的,則還可以進行 sort merge bucket map join。對於那個的建表語句爲:
CREATE TABLE my_user
(
uid INT,
name STRING
)
CLUSTERED BY (uid) SORTED BY (uid) into 32 buckets
STORED AS TEXTFILE;
這樣一來當兩邊 bucket 要做局部 join 的時候,只需要用類似 merge sort 算法中的 merge 操作一樣把兩個 bucket 順序遍歷一遍即可完成,這樣甚至都不用把一個 bucket 完整的加載成 hashtable,而且可以做全連接操作。
進行 sort merge bucket map join 時,需要設置的屬性爲:
1. `set hive.optimize.bucketmapjoin= true;`
2. `set hive.optimize.bucketmapjoin.sortedmerge = true;`
3. `set hive.input.format = org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat;`
4.各種 JOIN 對比
JOIN類型 | 優點 | 缺點 |
---|---|---|
COMMON JOIN | 可以完成各種 JOIN 操作,不受表大小和表格式的限制 | 無法只在 map 端完成 JOIN 操作,耗時長,佔用更多地網絡資源 |
MAP JOIN | 可以在 map 端完成 JOIN 操作,執行時間短 | 待連接的兩個表必須有一個“小表”,“小表”必須加載內存中 |
BUCKET MAP JOIN | 可以完成 MAP JOIN,不受“小表”限制 | 表必須分桶,做連接時小表分桶對應 hashtable 需要加載到內存 |
SORT MERGE BUCKET MAP JOIN | 執行時間短,可以做全連接,幾乎不受內存限制 | 表必須分桶,而且桶內數據有序 |
參考
本文轉載自:http://datavalley.github.io/2015/10/25/Hive%E4%B9%8BJOIN%E5%8F%8AJOIN%E4%BC%98%E5%8C%96