hive06_SQL最佳化

Stitches發表於2024-08-08

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

img

如何最佳化?

  • 針對 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

img

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 OperatorSelect OperatorJoin 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 完成。

img

Map Join:

  • 透過兩個只有 Map 階段的 Job 完成一個 join 操作,適用場景為大表 join 小表
  • 第一個 Job 會讀取小表資料,然後根據關聯欄位計算雜湊值,上傳到分散式快取的不同節點中(HDFS);
  • 第二個 Job 會從分散式快取中讀取對應小表資料,並快取在 MapTask 記憶體中,然後掃描大表資料,完成聯合操作。
  • 整個過程不需要 Reduce 操作,透過兩次 Map 操作減少了 shuffle 開銷。

img

Bucket Map Join:

  • 如果參與 join 的表均為分桶表,且關聯欄位為分桶欄位,且一張表的分桶數量是另一張表分桶數量的整數倍,此時就可以在兩表的分桶間進行 Map Join 操作。這樣第二個 Job 的 Map 端就不需要快取小表的全表資料了,而只需要快取其所需的分桶即可。分桶的目的是為了解決表資料無法快取到記憶體的問題。
  • 第一個 Job 會讀取小表分桶後的資料,然後上傳到分散式快取(HDFS);
  • 第二個 Job 首先會根據大表的分桶數啟動對應數量的 MapTask,每個 MapTask 從分散式快取讀取資料,只需要快取自己需要的小表桶資料。
  • Bucket Map Join 適用於大表與大表的聯合操作,透過分治思想將其中較小的表資料分桶。同時每次處理大表的一個桶資料,啟動一個 MapTask 任務,載入需要的小表桶資料到記憶體中,然後執行聯合操作,整個過程無需 Reduce 操作,避免了 shuffle 開銷。

img

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 不需要快取桶資料到記憶體,所以對記憶體沒有要求,另外因為資料是按照關聯欄位排序的,只需要順序讀取即可。

img

Map Join 最佳化

  • Hive 在編譯 SQL 語句階段,起初所有 join 操作均採用 Common join 實現。之後在執行階段,會根據每個 Common join 任務涉及的表大小判斷能夠轉化為 Map join 任務。
  • 但是存在一些特殊場景在編譯時不能確定涉及表大小,比如對子查詢執行 join 操作。針對這種情況,hive 會在編譯階段生成一個條件任務,它下面會包含一個計劃列表,計劃列表中包含轉換後的 Map join 任務以及原有的 Common join 任務。
    img

img

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 再聯合)。

案例:

現有以下三張表實現聯合操作:
img

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;

img

如圖每一步操作都會生成多個執行計劃,不同的執行計劃採取不同措施。

  • 方案二:無條件 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 任務。這種方式效率最高,但是佔用記憶體最多。

img

Bucket Map Join 最佳化

img

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 最佳化

img

如何觸發 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;

img

調整後的 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;

相關文章