HiveSQL 原理
join
join 分為 MapJoin、ReduceJoin 兩種,其中 MapJoin 思想是將小表存記憶體,然後大表分片,與小表完成連線操作。
MapJoin
- Map 階段分為兩個操作:
- 將小表資料讀入記憶體,生成分片檔案後儲存到分散式儲存系統中;
- 每個 Mapper 從分散式儲存系統中讀取檔案分片到記憶體,然後順序掃描大表,在 Map 階段完成 join;
這種方案只適用於一份小資料表和大表的聯合。
ReduceJoin
- Map 階段以關聯鍵作為 Key,查詢後的列作為 Value 輸出;
- shuffle 階段按照 Key 值hash 將資料傳送到不同 Reducer;
- Reduce 階段按照 Key 值執行聯合操作。
ReduceJoin 存在幾個問題:
- Map 階段沒有對資料瘦身,導致 Shuffle 階段傳輸資料量很大;
- Reduce 階段需要對 Value 做乘積運算,容易導致 OOM。
GroupBy
GroupBy
執行過程可以分為三個階段:
- map階段:將 groupby 後的欄位作為 key(多個就多個列看出整體),將 groupby 之後要進行聚合的欄位作為值,如果要進行
count(xxx)
,則值為1;如果sum(xxx)
,則值為該欄位;該階段能夠將資料平分給各個節點,總資料量維持恆定,所以不存在資料傾斜; - shuffle階段:按照 key 排序並將不同資料分發到指定 Reducer,shuffle 階段取決於 Map 的結果,無法控制。
- reduce階段:如果是 count 就統計各個 Key 的值累加之和,否則按照需要的聚合操作執行;Reduce 階段會因為 Shuffle 後各個節點的資料量不同產生資料傾斜問題,最終計算時間取決於資料量多的那個 Reduce。
with tmp1 as (
select
'a' as pro,
'1' as city
union all
select
'a',
'1'
union all
select
'a',
'1'
union all
select
'b',
'0'
)
select
pro,
city,
count(*)
from
tmp1
group by
pro,
city
如何最佳化?
- 針對 Key 做處理:
- 將 Key 替換為隨機數;
- 提前挑選出大數量級的 Key 單獨處理;
- 引數配置:
set hive.map.aggr = true
:開啟 Map 端的聚合操作,減少傳送到 Reducer 的資料量,同時需要設定hive.groupby.mapaggr.checkinterval
規定 Map 端進行聚合操作的數目;mapred.reduce.tasks
:執行 Reduce 任務的數量,配置為較大值來減少每個 Reducer 處理的數量;set hive.groupby.skewindata = true
:開啟自動的負載均衡,它會生成兩個 MR Job,第一個會隨機分佈 Map 的結果到不同 Reduce 中,這樣導致按照相同 Key 分組的資料會被髮送到不同的 Reduce 中;第二個 MR Job 按照 Key 進行正確分組;
Distinct 單欄位原理
- map階段:Distinct 執行時,Hive 會將操作轉換為一個 MapReduce 任務,並按照
groupBy
+distinct
欄位組合作為 Key,value 設定為 1 來作為 Map 任務輸出; - shuffle階段:將 GroupBy 的欄位作為分割槽鍵進行資料分割槽,傳送到不同 Reduce 任務;
- reduce階段:由於 Map 階段的排序,輸入天然就是按組合 Key 排好序的,此時按照順序取出組合鍵中的 distinct 欄位,依次遍歷 distinct 欄位找到每一個不同的值,如果是 count 操作,計數器就自增1,最終得到輸出。
with tmp1 as (
select
'a' as pro,
'1' as city,
'張三' as userid
union all
select
'a',
'1',
'張三'
union all
select
'a',
'1',
'張三'
union all
select
'b',
'0',
'張三'
)
select
pro,
count(distinct userid)
from
tmp1
group by
pro
count(distinct xxx)
會導致大量的資料傾斜,因為 Map 階段沒有完成資料去重操作,而是將 Map 輸出的結果交給一個 Reducer 處理執行 Count(xxx)
合併操作,這個操作會成為整個作業的 IO瓶頸。
如何最佳化?
https://blog.csdn.net/ioteye/article/details/107076218
在 Map 階段完成對資料的去重,可以巢狀子查詢,子查詢完成對 id欄位的去重(可併發執行),第二階段對去重後的 id 值合併計數,這樣 Map 階段輸出資料更少,Reduce 階段即使指定一個 Reducer 也不會成為效能瓶頸。
SELECT
COUNT(*)
FROM
(
SELECT
DISTINCT id
FROM
table_name
WHERE
....
) t;
Distinct 多欄位原理
https://mp.weixin.qq.com/s?__biz=Mzg4MTU5OTU0OQ==&mid=2247487792&idx=1&sn=5e097df0c88e0a0aa3f2a3d0b5103f62&chksm=cf622462f815ad74e0728dd7fae8c04ae2a219cacf959f16cc79eb7a25da8036ae744236bf3d&token=1889170053&lang=zh_CN#rd
SQL 調優
計算資源調優
針對 Hive On MR
,計算資源調整主要涉及 Yarn、MR。
YARN 配置: 主要包括 Container執行記憶體、CPU核數
yarn.nodemanager.resource.memory-mb
:每個NodeManager
節點分配給Container
使用的記憶體。yarn.nodemanager.resource.cpu-vcores
:每個NodeManager
節點分配給Container
使用的 CPU 核心數。yarn.scheduler.maximum-allocation-mb
:每個 Container 所能使用的最大記憶體。yarn.scheduler.minimum-allocation-mb
:每個 Container 所能使用的最小記憶體。
MapReduce 配置: 主要包括 MapTask 的記憶體和 CPU 核數、ReduceTask 的記憶體和 CPU 核數
mapreduce.map.memory.mb
:每個 MapTask 所使用的 Container 記憶體;mapreduce.map.cpu.vcores
:每個 MapTask 所使用的 Container CPU 核心數;mapreduce.reduce.memory.mb
:每個 ReduceTask 所使用的 Container 記憶體;mapreduce.reduce.cpu.vcores
:每個 ReduceTask 所使用的 Container CPU 核心數。
執行計劃調優
Explain 執行計劃:
- 由一系列 Stage 組成,每個 Stage 對應一個 MapReduce Job 或者檔案系統操作,各個 Stage 之間有先後順序。
- Map/Reduce 端計算邏輯分別由 Map操作樹、Reduce操作樹組成,一個操作代表某個階段的單一邏輯操作,比如
TableScan Operator
、Select Operator
、Join Operator
; - hive 中常見 Operator:
- TableScan:掃描表操作;
- Select Operator:選取;
- Group By Operator:分組;
- Reduce Output Operator:輸出;
- Filter Operator:過濾;
- Join Operator:連線;
- File Output Operator:檔案輸出;
- Fetch Operator:客戶端獲取資料操作。
Explain 語句模板: EXPLAIN [FORMATTED | EXTENDED | DEPENDENCY] query-sql
- FORMATTED:將執行計劃以 JSON 字串形式輸出;
- EXTENDED:輸出執行計劃中額外資訊,通常是讀寫的檔名等資訊;
- DENPENDENCY:輸出計劃中額外資訊,通常是讀寫檔名等資訊。
分組聚合最佳化
未最佳化執行過程: Hive 未經最佳化的分組聚合,是透過一個 MapReduce 任務完成的。Map 端負責讀取資料,並且按照分組欄位分割槽,透過 shuffle
,然後將資料傳送到 Reduce 端,各組資料在 Reduce 端完成最終的聚合運算。
最佳化後的執行過程: 最佳化主要圍繞減少 shuffle
資料量進行,具體做法是 map-side
聚合。在 Map 端記憶體中維護一個雜湊表,利用雜湊表完成部分結果的聚合,然後按照分組欄位分割槽,將結果傳送至 Reduce 端,完成最終聚合操作。map-side
的聚合操作能有效減少 shuffle 的資料量。
如何配置:
# 開啟 map-side
set hive.map.aggr=true
# 檢測是否有必要執行 map-side,若聚合後的條數和聚合前的條數比值小於該值,則認為適合做聚合,否則不適合
set hive.map.aggr.hash.min.reduction=0.5;
# 檢測源表是否適合 map-side 聚合的條數
set hive.groupby.mapaggr.checkinterval=100000;
# map-side 聚合使用的雜湊表佔用 Map端記憶體的最大比例,若超過該值執行一次雜湊表 flush 操作
set hive.map.aggr.hash.force.flush.memory.threshold=0.9;
分組聚合最佳化案例:
https://www.bilibili.com/video/BV1g84y147sX?p=128&spm_id_from=pageDriver&vd_source=e45643df6f84f8a621448f1e8f30eaf4
Join 最佳化
Hive 包括的 join 演算法:Common Join、Map Join、Bucket Map Join、Sort Merge Bucket Map Join;
Join 演算法介紹
Common Join:
- 最穩定的 Join 演算法,透過一個 MapReduce 任務完成。Map 端負責讀取 Join 操作所需要的表資料,並且按照關聯欄位分割槽,透過
shuffle
,然後將資料傳送到 Reduce 端,相同 Key 的資料最終在 Reduce 端完成最終的 Join 操作。 - 如果多表聯合 Join,關聯欄位一致,那麼只會透過一個 Commmon Join 任務完成,先讀取 Join 操作需要的表資料,然後透過
shuffle
將相同 Key 的資料傳送到 Reduce 端;如果關聯欄位不一致,則必須要透過多個 Common Join 任務。
SELECT
a.val,
b.val,
c.val
FROM a
JOIN b ON (a.key = b.key1)
JOIN c ON (c.key = b.key1)
兩個 join 操作均關聯到相同的欄位(b 表 key1),所以可以進行一次 shuffle,由一個 Common Join 任務完成。
SELECT
a.val,
b.val,
c.val
FROM a
JOIN b ON (a.key = b.key1)
JOIN c ON (c.key = b.key2)
兩個 join 操作關聯欄位不一致,所以必須兩次 Join 完成。
Map Join:
- 透過兩個只有 Map 階段的 Job 完成一個 join 操作,適用場景為大表 join 小表;
- 第一個 Job 會讀取小表資料,然後根據關聯欄位計算雜湊值,上傳到分散式快取的不同節點中(HDFS);
- 第二個 Job 會從分散式快取中讀取對應小表資料,並快取在 MapTask 記憶體中,然後掃描大表資料,完成聯合操作。
- 整個過程不需要 Reduce 操作,透過兩次 Map 操作減少了 shuffle 開銷。
Bucket Map Join:
- 如果參與 join 的表均為分桶表,且關聯欄位為分桶欄位,且一張表的分桶數量是另一張表分桶數量的整數倍,此時就可以在兩表的分桶間進行 Map Join 操作。這樣第二個 Job 的 Map 端就不需要快取小表的全表資料了,而只需要快取其所需的分桶即可。分桶的目的是為了解決表資料無法快取到記憶體的問題。
- 第一個 Job 會讀取小表分桶後的資料,然後上傳到分散式快取(HDFS);
- 第二個 Job 首先會根據大表的分桶數啟動對應數量的 MapTask,每個 MapTask 從分散式快取讀取資料,只需要快取自己需要的小表桶資料。
- Bucket Map Join 適用於大表與大表的聯合操作,透過分治思想將其中較小的表資料分桶。同時每次處理大表的一個桶資料,啟動一個 MapTask 任務,載入需要的小表桶資料到記憶體中,然後執行聯合操作,整個過程無需 Reduce 操作,避免了 shuffle 開銷。
Sort Merge Bucket Map Join:
- 要求參與聯合的表均為分桶表,且分桶內的資料均為有序的,且分桶欄位、排序欄位和關聯欄位為相同欄位,且一張表的分桶數是另一張表的分桶數的整數倍。
Bucket Map Join
的聯合原理是透過Hash Join
演算法;而Sort Merge Bucket Map Join
的原理是透過Sort Merge Join
演算法。Sort Merge Join
由於兩張表都是按照關聯欄位排好序的,所以無需將整張表的資料載入到記憶體,而只需要順序讀兩張表,對相同的資料值的資料行執行聯合操作。- Sort Merge Bucket Map Join 不需要快取桶資料到記憶體,所以對記憶體沒有要求,另外因為資料是按照關聯欄位排序的,只需要順序讀取即可。
Map Join 最佳化
- Hive 在編譯 SQL 語句階段,起初所有 join 操作均採用
Common join
實現。之後在執行階段,會根據每個Common join
任務涉及的表大小判斷能夠轉化為Map join
任務。 - 但是存在一些特殊場景在編譯時不能確定涉及表大小,比如對子查詢執行 join 操作。針對這種情況,hive 會在編譯階段生成一個條件任務,它下面會包含一個計劃列表,計劃列表中包含轉換後的
Map join
任務以及原有的Common join
任務。
Map Join
核心判斷邏輯:
hive.auto.convert.join
:是否自動最佳化 Common Join 任務;- 尋找大表候選人,根據 join 型別判斷能否做 Map Join。比如
a left join b
,只能將 b 作為快取表,這樣才能全量輸出 a 表資料,在此基礎上判斷此時能否進行 Map Join 最佳化。 hive.auto.convert.join.noconditionaltask = false
:需要開啟條件任務。條件任務會以每個大表候選人作為大表,生成 Map join 計劃,若某大表候選人大小已知,且其之外的已知的表大小總和大於hive.mapjoin.smalltable.filesize
,則不生成對應的 Map Join 計劃。hive.auto.convert.join.noconditionaltask = true
:不需要開啟條件任務,則除開大表候選人之外其餘表大小必須已知,且總和小於hive.auto.convert.join.noconditionaltask.size
,如果滿足生成最優的 Map Join 計劃。若不滿足上述條件,還是會生成條件任務。- 若已經生成最優 Map Join 計劃,此時子任務也是 Map Join,並且子任務和當前任務的所有小表大小均已知,並且總和小於
hive.auto.convert.join.noconditionaltask.size
,則會將當前 Map Join 和子任務的 Map Join 合併為一個任務。即使此時出現多表聯合的情況,比如 a、b、c 三表聯合,如果是 Common Join,一定為生成兩個 Join 任務執行,同時過程中含有 shuffle;如果是 Map Join,則變成 a-b、b-c 兩個 Map Join 任務,最終可以合併為一個 Map Join 任務(a 和 b 先聯合,然後根據結果和 c 再聯合)。
案例:
現有以下三張表實現聯合操作:
select
*
from order_detail od
join product_info product on od.product_id = product.id
join province_info province on od.province_id = province.id;
- 方案一:MapJoin 最佳化
# 配置 MapJoin 自動轉換開啟
set hive.auto.convert.join=true;
# 不使用無條件轉換 MapJoin
set hive.auto.convert.join.noconditionaltask=false;
# 調整記憶體大小,使其大於 product_info 表大小
set hive.mapjoin.smalltable.filesize=25285707;
如圖每一步操作都會生成多個執行計劃,不同的執行計劃採取不同措施。
- 方案二:無條件 MapJoin 最佳化
# 配置 MapJoin 自動轉換
set hive.auto.convert.join=true;
# 使用無條件轉換
set hive.auto.convert.join.noconditionaltask=true;
# 調整記憶體大小,使其大於等於 product_info、province_info 表大小之和
set hive.auto.convert.join.noconditionaltask.size=25286076;
上述配置會轉化為兩個 MapJoin Operator,並且由於兩個 MapJoin Operator 小表之和小於記憶體大小,所以可以合併為一個 MapJoin 任務。這種方式效率最高,但是佔用記憶體最多。
Bucket Map Join 最佳化
SELECT
*
FROM (
SELECT
*
FROM order_detail where dt = '2020-06-14'
) od
JOIN
(
SELECT
*
FROM payment_detail
WHERE dt = '2020-06-14';
) pd
ON od.id = pd.order_detail_id;
分析:
第二張表大小為 300M,按一般壓縮率為 10:1 來算,需要至少 3G 記憶體才能儲存雜湊表資料。因此 Map Join 最佳化不現實,考慮 Bukcet Map Join 最佳化。
考慮將order_detail
拆分為 16 個桶,payment_detail
拆分為 8 個桶,兩個表的桶數成倍數關係。
- 建表語句
# 刪除分桶表
drop table if exists order_detail_bucketed;
# 重新建立分桶表
create table order_detail_bucketed (
id string comment '訂單ID',
user_id string comment '使用者ID',
product_id string comment '商品ID',
province_id string comment '省份ID',
create_time string comment '建立時間',
product_number int comment '商品數量',
total_amount decimal(16, 2) comment '訂單金額'
)
clustered by (id) into 16 buckets
row format delimited fields terminated by '\t';
drop table if exists payment_detail_bucketed;
create table payment_detail_bucketed(
id string comment '支付id',
order_detail_id string comment '訂單明細id',
user_id string comment '使用者id',
payment_time string comment '支付時間',
total_amount decimal(16, 2) comment '支付金額'
)
clustered by (order_detail_id) into 8 buckets
row format delimited fields terminated by '\t';
- 匯入資料到各個分桶表
insert overwrite table order_detail_bucketed
select
id,
user_id,
product_id,
province_id,
create_time,
product_num,
total_amount
from order_detail
where dt='2020-06-14';
insert overwrite table payment_detail_bucketed
select
id,
order_detail_id,
user_id,
payment_time,
total_amount
from payment_detail
where dt='2020-06-14';
- 開啟 Bucket Map Join 配置
# 關閉 cbo 最佳化,cbo 導致 Bucket Map Join 失效
set hive.cbo.enable=false;
# 啟用 Bucket Map Join
set hive.optimize.bucketmapjoin=true;
- 重寫 SQL 如下
select /*+ mapjoin(pd) */
*
from order_detail_bucketd od
join payment_detail_bucketed pd on od.id = pd.order_detail_id;
Sort Merge Bucket Map Join 最佳化
如何觸發 sort merge bucket map join
:
set hive.optimize.bucketmapjoin.sortedmerge = true;
:啟動 Sort Merge Bucket Map Join 最佳化;set hive.auto.convert.sortmerge.join = true;
:開啟自動轉換 Sort Merge Bucket Map Join 最佳化;
最佳化前:
select
*
from (
select * from order_detail
where dt = '2020-06-14'
) od
join (
select * from payment_detail
where dt = '2020-06-14'
) pd
on od.id = pd.order_detail_id;
包含一個 join 語句,因此執行一個 Common Join
任務,透過一個 MapReduce Job
實現。
參考上述資料,最小的資料大小為 319M,預估需要 3G 的記憶體才能成功執行。因此最好使用 Bucket Map Join
,但是 Sort Merge Bucket Map Join
最佳化後對分桶大小沒有要求,佔用記憶體更小。
依舊是按照 Bucket Map Join
最佳化方式先建立分桶表、匯入資料,然後配置如下引數開啟最佳化:
set hive.optimize.bucketmapjoin.sortedmerge = true;
:開啟 Sort Merge Bucket Map Join 最佳化;set hive.auto.convert.sortmerge.join = true;
:開啟自動轉換 Sort Merge Bucket Map Join 最佳化;
最後重寫 SQL:
select
*
from order_detail_sorted_bucketed od
join payment_detail_sorted_bucketed pd
on od.id = pd.order_detail_id;
資料傾斜調優
分組聚合導致資料傾斜
比如執行 groupby
等操作會導致 Map 操作後,相同 key 的資料傳送到同一個 Reduce 節點,從而導致資料傾斜。
方案一:Map端聚合
原理:資料分割槽後,Map端再執行資料聚合,減少了傳送到 Reduce 節點的資料量。
# 開啟 Map 端聚合
set hive.map.aggr = true;
# 檢測是否有必要執行 map-side,聚合後的資料條數/聚合前的資料條數比值小於該值,認為適合聚合
set hive.map.aggr.hash.min.reduction = xxx;
# MapTask 記憶體大於該值會執行一次 flush
set hive.map.hash.force.flush.memory.threshold = xxx;
方案二:Skew-GroupBy聚合
原理:啟動兩個 MR 任務,一個按照隨機數分割槽,將資料隨機傳送到 Reduce;一個按照分組欄位分割槽,完成最終聚合。
set hive.groupby.skewindata = true;
Join 導致的資料傾斜
前文提到的 join 實際上是透過一個 MapReduce 任務完成的,預設是使用 Common Join 演算法,Map端負責讀取目標欄位並按照關聯欄位分割槽,然後透過 shuffle 將資料傳送到 Reduce,最終相同的 Key 完成聯合操作。這樣就可能出現某個關聯欄位值的資料量非常大,都會 shuffle 傳送到同一個 Reduce節點上。
方案一:Map Join
原理:適合大小表聯合,Join 操作在 Map 端完成,沒有 shuffle 的效能開銷。
# 開啟 MapJoin 自動轉換
set hive.auto.convert.join = true;
# 記憶體限制,判斷是否可以生成 MapJoin 任務
set hive.mapjoin.smalltable.filesize = xxx;
# 開啟無條件轉換 MapJoin,否則會生成多個可能的執行計劃
set hive.auto.convert.join.noconditionaltask = true;
# 無條件轉換為 MapJoin 開啟後,記憶體大小閾值,若小於該閾值,不會再生成備用計劃
set hive.auto.convert.join.noconditionaltask.size = xxx;
方案二:Skew Join
原理:為傾斜的大 Key 單獨啟動一個 Map Join 任務計算,其餘 Key 進行正常的 Common Join。
# 啟動 Skew Join 最佳化
set hive.optimize.skewjoin = true;
# 觸發 Skew Join 的閾值,若某個 Key 行數超過該值則觸發
set hive.skewjoin.key = 10000;
方案三:調整 SQL 語句
假設存在資料傾斜問題如下:
select * from A join B on A.id = B.id;
調整後的 sql 語句如下:
select
*
from (
select // 按照id欄位打散
concat(id, '_', cast(rand() * 2 as int)) id,
value
from A
) ta
join (
select // 小表資料擴容
concat(id, '_', 0) id,
value
from B
union all
select
concat(id, '_', 1) id,
value
from B
) tb
on ta.id = tb.id;
分別對存在資料傾斜的表的值加隨機數打散(concat),再對小表資料進行擴容(union all),完成對 SQL 的最佳化。
任務並行度最佳化
Map 端並行度
Map 端並行度是由 Map 的個數決定的,取決於輸入檔案的切片數,一般情況下,Map端的並行度無需手動調整。
- 小檔案最佳化
如果 Map 端存在過多的小檔案,會導致啟動大量的 MapTask,所以需要對小檔案進行合併。透過配置 set hive.input.format = org.apache.hadoop.hive.ql.io.HiveInputFormat;
控制 MapTask 的個數。
- 增加MapTask數量
在計算資源充足的前提下,可以增大 Map 端並行度(MapTask 數量),每個 MapTask 計算資料減少一些。透過配置 set mapreduce.input.fileinputformat.split.maxsize = xxx;
Reduce 端並行度
# 指定 Reduce 端並行度,如果值為-1,表示使用者未指定
set mapreduce.job.reduces = xxx;
# Reduce 端並行度最大值
set hive.exec.reducers.max = xxx;
# 單個 Reduce Task 計算的資料量
set hive.exec.reducers.bytes.per.reducer = xxx;
參考:
Hive_On_Spark 和 Spark_On_Hive 區別:https://www.cnblogs.com/liugp/p/16209394.html;