Hive作為大數據領域常用的數據倉庫組件,在平時設計和查詢時要特別注意效率。影響Hive效率的幾乎從不是數據量過大,而是數據傾斜、數據冗余、job或I/O過多、MapReduce分配不合理等等。對Hive的調優既包含對HiveQL語句本身的優化,也包含Hive配置項和MR方面的調整。
由于在寫的過程中發現篇幅過長,因此決定拆成上下兩篇發布。上篇包含從開頭到join優化的內容,下篇的傳送門是http://www.lxweimin.com/p/deb4a6f91d3b。祝食用愉快。
目錄
- 列裁剪和分區裁剪
- 謂詞下推
- sort by代替order by
- group by代替distinct
- group by配置調整
- map端預聚合
- 傾斜均衡配置項
- join基礎優化
- build table(小表)前置
- 多表join時key相同
- 利用map join特性
- 分桶表map join
- 傾斜均衡配置項
- 優化SQL處理join數據傾斜
- 空值或無意義值
- 單獨處理傾斜key
- 不同數據類型
- build table過大
- MapReduce優化
- 調整mapper數
- 調整reducer數
- 合并小文件
- 啟用壓縮
- JVM重用
- 并行執行與本地模式
- 嚴格模式
- 采用合適的存儲格式
列裁剪和分區裁剪
最基本的操作。所謂列裁剪就是在查詢時只讀取需要的列,分區裁剪就是只讀取需要的分區。以我們的日歷記錄表為例:
select uid,event_type,record_data
from calendar_record_log
where pt_date >= 20190201 and pt_date <= 20190224
and status = 0;
當列很多或者數據量很大時,如果select *或者不指定分區,全列掃描和全表掃描效率都很低。
Hive中與列裁剪優化相關的配置項是hive.optimize.cp
,與分區裁剪優化相關的則是hive.optimize.pruner
,默認都是true。在HiveQL解析階段對應的則是ColumnPruner邏輯優化器。
謂詞下推
在關系型數據庫如MySQL中,也有謂詞下推(Predicate Pushdown,PPD)的概念。它就是將SQL語句中的where謂詞邏輯都盡可能提前執行,減少下游處理的數據量。
例如以下HiveQL語句:
select a.uid,a.event_type,b.topic_id,b.title
from calendar_record_log a
left outer join (
select uid,topic_id,title from forum_topic
where pt_date = 20190224 and length(content) >= 100
) b on a.uid = b.uid
where a.pt_date = 20190224 and status = 0;
對forum_topic做過濾的where語句寫在子查詢內部,而不是外部。Hive中有謂詞下推優化的配置項hive.optimize.ppd
,默認值true,與它對應的邏輯優化器是PredicatePushDown。該優化器就是將OperatorTree中的FilterOperator向上提,見下圖。
上面的鏈接中是一篇講解HiveQL解析與執行過程的好文章,前文提到的優化器、OperatorTree等概念在其中也有詳細的解釋,非常推薦。
sort by代替order by
HiveQL中的order by與其他SQL方言中的功能一樣,就是將結果按某字段全局排序,這會導致所有map端數據都進入一個reducer中,在數據量大時可能會長時間計算不完。
如果使用sort by,那么還是會視情況啟動多個reducer進行排序,并且保證每個reducer內局部有序。為了控制map端數據分配到reducer的key,往往還要配合distribute by一同使用。如果不加distribute by的話,map端數據就會隨機分配到reducer。
舉個例子,假如要以UID為key,以上傳時間倒序、記錄類型倒序輸出記錄數據:
select uid,upload_time,event_type,record_data
from calendar_record_log
where pt_date >= 20190201 and pt_date <= 20190224
distribute by uid
sort by upload_time desc,event_type desc;
group by代替distinct
當要統計某一列的去重數時,如果數據量很大,count(distinct)就會非常慢,原因與order by類似,count(distinct)邏輯只會有很少的reducer來處理。這時可以用group by來改寫:
select count(1) from (
select uid from calendar_record_log
where pt_date >= 20190101
group by uid
) t;
但是這樣寫會啟動兩個MR job(單純distinct只會啟動一個),所以要確保數據量大到啟動job的overhead遠小于計算耗時,才考慮這種方法。當數據集很小或者key的傾斜比較明顯時,group by還可能會比distinct慢。
那么如何用group by方式同時統計多個列?下面是解決方法:
select t.a,sum(t.b),count(t.c),count(t.d) from (
select a,b,null c,null d from some_table
union all
select a,0 b,c,null d from some_table group by a,c
union all
select a,0 b,null c,d from some_table group by a,d
) t;
group by配置調整
map端預聚合
group by時,如果先起一個combiner在map端做部分預聚合,可以有效減少shuffle數據量。預聚合的配置項是hive.map.aggr
,默認值true,對應的優化器為GroupByOptimizer,簡單方便。
通過hive.groupby.mapaggr.checkinterval
參數也可以設置map端預聚合的行數閾值,超過該值就會分拆job,默認值100000。
傾斜均衡配置項
group by時如果某些key對應的數據量過大,就會發生數據傾斜。Hive自帶了一個均衡數據傾斜的配置項hive.groupby.skewindata
,默認值false。
其實現方法是在group by時啟動兩個MR job。第一個job會將map端數據隨機輸入reducer,每個reducer做部分聚合,相同的key就會分布在不同的reducer中。第二個job再將前面預處理過的數據按key聚合并輸出結果,這樣就起到了均衡的效果。
但是,配置項畢竟是死的,單純靠它有時不能根本上解決問題,因此還是建議自行了解數據傾斜的細節,并優化查詢語句。
join基礎優化
join優化是一個復雜的話題,下面先說5點最基本的注意事項。
build table(小表)前置
在最常見的hash join方法中,一般總有一張相對小的表和一張相對大的表,小表叫build table,大表叫probe table。如下圖所示。
Hive在解析帶join的SQL語句時,會默認將最后一個表作為probe table,將前面的表作為build table并試圖將它們讀進內存。如果表順序寫反,probe table在前面,引發OOM的風險就高了。
在維度建模數據倉庫中,事實表就是probe table,維度表就是build table。假設現在要將日歷記錄事實表和記錄項編碼維度表來join:
select a.event_type,a.event_code,a.event_desc,b.upload_time
from calendar_event_code a
inner join (
select event_type,upload_time from calendar_record_log
where pt_date = 20190225
) b on a.event_type = b.event_type;
多表join時key相同
這種情況會將多個join合并為一個MR job來處理,例如:
select a.event_type,a.event_code,a.event_desc,b.upload_time
from calendar_event_code a
inner join (
select event_type,upload_time from calendar_record_log
where pt_date = 20190225
) b on a.event_type = b.event_type
inner join (
select event_type,upload_time from calendar_record_log_2
where pt_date = 20190225
) c on a.event_type = c.event_type;
如果上面兩個join的條件不相同,比如改成a.event_code = c.event_code
,就會拆成兩個MR job計算。
負責這個的是相關性優化器CorrelationOptimizer,它的功能除此之外還非常多,邏輯復雜,參考Hive官方的文檔可以獲得更多細節:https://cwiki.apache.org/confluence/display/Hive/Correlation+Optimizer。
利用map join特性
map join特別適合大小表join的情況。Hive會將build table和probe table在map端直接完成join過程,消滅了reduce,效率很高。
select /*+mapjoin(a)*/ a.event_type,b.upload_time
from calendar_event_code a
inner join (
select event_type,upload_time from calendar_record_log
where pt_date = 20190225
) b on a.event_type < b.event_type;
上面的語句中加了一條map join hint,以顯式啟用map join特性。早在Hive 0.8版本之后,就不需要寫這條hint了。map join還支持不等值連接,應用更加靈活。
map join的配置項是hive.auto.convert.join
,默認值true,對應邏輯優化器是MapJoinProcessor。
還有一些參數用來控制map join的行為,比如hive.mapjoin.smalltable.filesize
,當build table大小小于該值就會啟用map join,默認值25000000(25MB)。還有hive.mapjoin.cache.numrows
,表示緩存build table的多少行數據到內存,默認值25000。
分桶表map join
map join對分桶表還有特別的優化。由于分桶表是基于一列進行hash存儲的,因此非常適合抽樣(按桶或按塊抽樣)。
它對應的配置項是hive.optimize.bucketmapjoin
,優化器是BucketMapJoinOptimizer。但我們的業務中用分桶表較少,所以就不班門弄斧了,只是提一句。
傾斜均衡配置項
這個配置與上面group by的傾斜均衡配置項異曲同工,通過hive.optimize.skewjoin
來配置,默認false。
如果開啟了,在join過程中Hive會將計數超過閾值hive.skewjoin.key
(默認100000)的傾斜key對應的行臨時寫進文件中,然后再啟動另一個job做map join生成結果。通過hive.skewjoin.mapjoin.map.tasks
參數還可以控制第二個job的mapper數量,默認10000。
再重復一遍,通過自帶的配置項經常不能解決數據傾斜問題。join是數據傾斜的重災區,后面還要介紹在SQL層面處理傾斜的各種方法。