HiveSQL常用优化方法全面总结
轉(zhuǎn)載自??HiveSQL常用優(yōu)化方法全面總結(jié)
Hive作為大數(shù)據(jù)領(lǐng)域常用的數(shù)據(jù)倉(cāng)庫(kù)組件,在平時(shí)設(shè)計(jì)和查詢時(shí)要特別注意效率。影響Hive效率的幾乎從不是數(shù)據(jù)量過(guò)大,而是數(shù)據(jù)傾斜、數(shù)據(jù)冗余、job或I/O過(guò)多、MapReduce分配不合理等等。對(duì)Hive的調(diào)優(yōu)既包含對(duì)HiveQL語(yǔ)句本身的優(yōu)化,也包含Hive配置項(xiàng)和MR方面的調(diào)整。
?
列裁剪和分區(qū)裁剪
最基本的操作。所謂列裁剪就是在查詢時(shí)只讀取需要的列,分區(qū)裁剪就是只讀取需要的分區(qū)。以我們的日歷記錄表為例:
當(dāng)列很多或者數(shù)據(jù)量很大時(shí),如果select *或者不指定分區(qū),全列掃描和全表掃描效率都很低。
Hive中與列裁剪優(yōu)化相關(guān)的配置項(xiàng)是hive.optimize.cp,與分區(qū)裁剪優(yōu)化相關(guān)的則是hive.optimize.pruner,默認(rèn)都是true。在HiveQL解析階段對(duì)應(yīng)的則是ColumnPruner邏輯優(yōu)化器。
?
謂詞下推
在關(guān)系型數(shù)據(jù)庫(kù)如MySQL中,也有謂詞下推(Predicate Pushdown,PPD)的概念。它就是將SQL語(yǔ)句中的where謂詞邏輯都盡可能提前執(zhí)行,減少下游處理的數(shù)據(jù)量。
例如以下HiveQL語(yǔ)句:
對(duì)forum_topic做過(guò)濾的where語(yǔ)句寫在子查詢內(nèi)部,而不是外部。Hive中有謂詞下推優(yōu)化的配置項(xiàng)hive.optimize.ppd,默認(rèn)值true,與它對(duì)應(yīng)的邏輯優(yōu)化器是PredicatePushDown。該優(yōu)化器就是將OperatorTree中的FilterOperator向上提,見(jiàn)下圖。
圖來(lái)自https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html
?
上面的鏈接中是一篇講解HiveQL解析與執(zhí)行過(guò)程的好文章,前文提到的優(yōu)化器、OperatorTree等概念在其中也有詳細(xì)的解釋,非常推薦。
?
sort by代替order by
HiveQL中的order by與其他SQL方言中的功能一樣,就是將結(jié)果按某字段全局排序,這會(huì)導(dǎo)致所有map端數(shù)據(jù)都進(jìn)入一個(gè)reducer中,在數(shù)據(jù)量大時(shí)可能會(huì)長(zhǎng)時(shí)間計(jì)算不完。
如果使用sort by,那么還是會(huì)視情況啟動(dòng)多個(gè)reducer進(jìn)行排序,并且保證每個(gè)reducer內(nèi)局部有序。為了控制map端數(shù)據(jù)分配到reducer的key,往往還要配合distribute by一同使用。如果不加distribute by的話,map端數(shù)據(jù)就會(huì)隨機(jī)分配到reducer。
舉個(gè)例子,假如要以UID為key,以上傳時(shí)間倒序、記錄類型倒序輸出記錄數(shù)據(jù):
group by代替distinct
當(dāng)要統(tǒng)計(jì)某一列的去重?cái)?shù)時(shí),如果數(shù)據(jù)量很大,count(distinct)就會(huì)非常慢,原因與order by類似,count(distinct)邏輯只會(huì)有很少的reducer來(lái)處理。這時(shí)可以用group by來(lái)改寫:
select count(1) from (select uid from calendar_record_logwhere pt_date >= 20190101group by uid ) t;但是這樣寫會(huì)啟動(dòng)兩個(gè)MR job(單純distinct只會(huì)啟動(dòng)一個(gè)),所以要確保數(shù)據(jù)量大到啟動(dòng)job的overhead遠(yuǎn)小于計(jì)算耗時(shí),才考慮這種方法。當(dāng)數(shù)據(jù)集很小或者key的傾斜比較明顯時(shí),group by還可能會(huì)比distinct慢。
那么如何用group by方式同時(shí)統(tǒng)計(jì)多個(gè)列?下面是解決方法:
?
select t.a,sum(t.b),count(t.c),count(t.d) from (select a,b,null c,null d from some_tableunion allselect a,0 b,c,null d from some_table group by a,cunion allselect a,0 b,null c,d from some_table group by a,d ) t;group by配置調(diào)整
map端預(yù)聚合
group by時(shí),如果先起一個(gè)combiner在map端做部分預(yù)聚合,可以有效減少shuffle數(shù)據(jù)量。預(yù)聚合的配置項(xiàng)是hive.map.aggr,默認(rèn)值true,對(duì)應(yīng)的優(yōu)化器為GroupByOptimizer,簡(jiǎn)單方便。
通過(guò)hive.groupby.mapaggr.checkinterval參數(shù)也可以設(shè)置map端預(yù)聚合的行數(shù)閾值,超過(guò)該值就會(huì)分拆job,默認(rèn)值100000。
傾斜均衡配置項(xiàng)
group by時(shí)如果某些key對(duì)應(yīng)的數(shù)據(jù)量過(guò)大,就會(huì)發(fā)生數(shù)據(jù)傾斜。Hive自帶了一個(gè)均衡數(shù)據(jù)傾斜的配置項(xiàng)hive.groupby.skewindata,默認(rèn)值false。
其實(shí)現(xiàn)方法是在group by時(shí)啟動(dòng)兩個(gè)MR job。第一個(gè)job會(huì)將map端數(shù)據(jù)隨機(jī)輸入reducer,每個(gè)reducer做部分聚合,相同的key就會(huì)分布在不同的reducer中。第二個(gè)job再將前面預(yù)處理過(guò)的數(shù)據(jù)按key聚合并輸出結(jié)果,這樣就起到了均衡的效果。
但是,配置項(xiàng)畢竟是死的,單純靠它有時(shí)不能根本上解決問(wèn)題,因此還是建議自行了解數(shù)據(jù)傾斜的細(xì)節(jié),并優(yōu)化查詢語(yǔ)句。
join基礎(chǔ)優(yōu)化
join優(yōu)化是一個(gè)復(fù)雜的話題,下面先說(shuō)5點(diǎn)最基本的注意事項(xiàng)。
build table(小表)前置
在最常見(jiàn)的hash join方法中,一般總有一張相對(duì)小的表和一張相對(duì)大的表,小表叫build table,大表叫probe table。如下圖所示。
Hive在解析帶join的SQL語(yǔ)句時(shí),會(huì)默認(rèn)將最后一個(gè)表作為probe table,將前面的表作為build table并試圖將它們讀進(jìn)內(nèi)存。如果表順序?qū)懛?#xff0c;probe table在前面,引發(fā)OOM的風(fēng)險(xiǎn)就高了。
在維度建模數(shù)據(jù)倉(cāng)庫(kù)中,事實(shí)表就是probe table,維度表就是build table。假設(shè)現(xiàn)在要將日歷記錄事實(shí)表和記錄項(xiàng)編碼維度表來(lái)join:
多表join時(shí)key相同
這種情況會(huì)將多個(gè)join合并為一個(gè)MR job來(lái)處理,例如:
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_logwhere pt_date = 20190225 ) b on a.event_type = b.event_type inner join (select event_type,upload_time from calendar_record_log_2where pt_date = 20190225 ) c on a.event_type = c.event_type;如果上面兩個(gè)join的條件不相同,比如改成a.event_code = c.event_code,就會(huì)拆成兩個(gè)MR job計(jì)算。
負(fù)責(zé)這個(gè)的是相關(guān)性優(yōu)化器CorrelationOptimizer,它的功能除此之外還非常多,邏輯復(fù)雜,參考Hive官方的文檔可以獲得更多細(xì)節(jié):https://cwiki.apache.org/confluence/display/Hive/Correlation+Optimizer。
利用map join特性
map join特別適合大小表join的情況。Hive會(huì)將build table和probe table在map端直接完成join過(guò)程,消滅了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_logwhere pt_date = 20190225 ) b on a.event_type < b.event_type;上面的語(yǔ)句中加了一條map join hint,以顯式啟用map join特性。早在Hive 0.8版本之后,就不需要寫這條hint了。map join還支持不等值連接,應(yīng)用更加靈活。
map join的配置項(xiàng)是hive.auto.convert.join,默認(rèn)值true,對(duì)應(yīng)邏輯優(yōu)化器是MapJoinProcessor。
還有一些參數(shù)用來(lái)控制map join的行為,比如hive.mapjoin.smalltable.filesize,當(dāng)build table大小小于該值就會(huì)啟用map join,默認(rèn)值25000000(25MB)。還有hive.mapjoin.cache.numrows,表示緩存build table的多少行數(shù)據(jù)到內(nèi)存,默認(rèn)值25000。
分桶表map join
map join對(duì)分桶表還有特別的優(yōu)化。由于分桶表是基于一列進(jìn)行hash存儲(chǔ)的,因此非常適合抽樣(按桶或按塊抽樣)。
它對(duì)應(yīng)的配置項(xiàng)是hive.optimize.bucketmapjoin,優(yōu)化器是BucketMapJoinOptimizer。但我們的業(yè)務(wù)中用分桶表較少,所以就不班門弄斧了,只是提一句。
傾斜均衡配置項(xiàng)
這個(gè)配置與上面group by的傾斜均衡配置項(xiàng)異曲同工,通過(guò)hive.optimize.skewjoin來(lái)配置,默認(rèn)false。
如果開啟了,在join過(guò)程中Hive會(huì)將計(jì)數(shù)超過(guò)閾值hive.skewjoin.key(默認(rèn)100000)的傾斜key對(duì)應(yīng)的行臨時(shí)寫進(jìn)文件中,然后再啟動(dòng)另一個(gè)job做map join生成結(jié)果。通過(guò)hive.skewjoin.mapjoin.map.tasks參數(shù)還可以控制第二個(gè)job的mapper數(shù)量,默認(rèn)10000。
再重復(fù)一遍,通過(guò)自帶的配置項(xiàng)經(jīng)常不能解決數(shù)據(jù)傾斜問(wèn)題。join是數(shù)據(jù)傾斜的重災(zāi)區(qū),后面還要介紹在SQL層面處理傾斜的各種方法。
優(yōu)化SQL處理join數(shù)據(jù)傾斜
上面已經(jīng)多次提到了數(shù)據(jù)傾斜,包括已經(jīng)寫過(guò)的sort by代替order by,以及group by代替distinct方法,本質(zhì)上也是為了解決它。join操作更是數(shù)據(jù)傾斜的重災(zāi)區(qū),需要多加注意。
空值或無(wú)意義值
這種情況很常見(jiàn),比如當(dāng)事實(shí)表是日志類數(shù)據(jù)時(shí),往往會(huì)有一些項(xiàng)沒(méi)有記錄到,我們視情況會(huì)將它置為null,或者空字符串、-1等。如果缺失的項(xiàng)很多,在做join時(shí)這些空值就會(huì)非常集中,拖累進(jìn)度。
因此,若不需要空值數(shù)據(jù),就提前寫where語(yǔ)句過(guò)濾掉。需要保留的話,將空值key用隨機(jī)方式打散,例如將用戶ID為null的記錄隨機(jī)改為負(fù)值:
單獨(dú)處理傾斜key
這其實(shí)是上面處理空值方法的拓展,不過(guò)傾斜的key變成了有意義的。一般來(lái)講傾斜的key都很少,我們可以將它們抽樣出來(lái),對(duì)應(yīng)的行單獨(dú)存入臨時(shí)表中,然后打上一個(gè)較小的隨機(jī)數(shù)前綴(比如0~9),最后再進(jìn)行聚合。SQL語(yǔ)句與上面的相仿,不再贅述。
不同數(shù)據(jù)類型
這種情況不太常見(jiàn),主要出現(xiàn)在相同業(yè)務(wù)含義的列發(fā)生過(guò)邏輯上的變化時(shí)。
舉個(gè)例子,假如我們有一舊一新兩張日歷記錄表,舊表的記錄類型字段是(event_type int),新表的是(event_type string)。為了兼容舊版記錄,新表的event_type也會(huì)以字符串形式存儲(chǔ)舊版的值,比如'17'。當(dāng)這兩張表join時(shí),經(jīng)常要耗費(fèi)很長(zhǎng)時(shí)間。其原因就是如果不轉(zhuǎn)換類型,計(jì)算key的hash值時(shí)默認(rèn)是以int型做的,這就導(dǎo)致所有“真正的”string型key都分配到一個(gè)reducer上。所以要注意類型轉(zhuǎn)換:
build table過(guò)大
有時(shí),build table會(huì)大到無(wú)法直接使用map join的地步,比如全量用戶維度表,而使用普通join又有數(shù)據(jù)分布不均的問(wèn)題。這時(shí)就要充分利用probe table的限制條件,削減build table的數(shù)據(jù)量,再使用map join解決。代價(jià)就是需要進(jìn)行兩次join。舉個(gè)例子:
select /*+mapjoin(b)*/ a.uid,a.event_type,b.status,b.extra_info from calendar_record_log a left outer join (select /*+mapjoin(s)*/ t.uid,t.status,t.extra_infofrom (select distinct uid from calendar_record_log where pt_date = 20190228) sinner join user_info t on s.uid = t.uid ) b on a.uid = b.uid where a.pt_date = 20190228;MapReduce優(yōu)化
調(diào)整mapper數(shù)
mapper數(shù)量與輸入文件的split數(shù)息息相關(guān),在Hadoop源碼org.apache.hadoop.mapreduce.lib.input.FileInputFormat類中可以看到split劃分的具體邏輯。這里不貼代碼,直接敘述mapper數(shù)是如何確定的。
-
可以直接通過(guò)參數(shù)mapred.map.tasks(默認(rèn)值2)來(lái)設(shè)定mapper數(shù)的期望值,但它不一定會(huì)生效,下面會(huì)提到。
-
設(shè)輸入文件的總大小為total_input_size。HDFS中,一個(gè)塊的大小由參數(shù)dfs.block.size指定,默認(rèn)值64MB或128MB。在默認(rèn)情況下,mapper數(shù)就是:
default_mapper_num = total_input_size / dfs.block.size。 -
參數(shù)mapred.min.split.size(默認(rèn)值1B)和mapred.max.split.size(默認(rèn)值64MB)分別用來(lái)指定split的最小和最大大小。split大小和split數(shù)計(jì)算規(guī)則是:
split_size = MAX(mapred.min.split.size, MIN(mapred.max.split.size, dfs.block.size));
split_num = total_input_size / split_size。 -
得出mapper數(shù):
mapper_num = MIN(split_num, MAX(default_num, mapred.map.tasks))。
可見(jiàn),如果想減少mapper數(shù),就適當(dāng)調(diào)高mapred.min.split.size,split數(shù)就減少了。如果想增大mapper數(shù),除了降低mapred.min.split.size之外,也可以調(diào)高mapred.map.tasks。
一般來(lái)講,如果輸入文件是少量大文件,就減少mapper數(shù);如果輸入文件是大量非小文件,就增大mapper數(shù);至于大量小文件的情況,得參考下面“合并小文件”一節(jié)的方法處理。
調(diào)整reducer數(shù)
reducer數(shù)量的確定方法比mapper簡(jiǎn)單得多。使用參數(shù)mapred.reduce.tasks可以直接設(shè)定reducer數(shù)量,不像mapper一樣是期望值。但如果不設(shè)這個(gè)參數(shù)的話,Hive就會(huì)自行推測(cè),邏輯如下:
-
參數(shù)hive.exec.reducers.bytes.per.reducer用來(lái)設(shè)定每個(gè)reducer能夠處理的最大數(shù)據(jù)量,默認(rèn)值1G(1.2版本之前)或256M(1.2版本之后)。
-
參數(shù)hive.exec.reducers.max用來(lái)設(shè)定每個(gè)job的最大reducer數(shù)量,默認(rèn)值999(1.2版本之前)或1009(1.2版本之后)。
-
得出reducer數(shù):
reducer_num = MIN(total_input_size / reducers.bytes.per.reducer, reducers.max)。
reducer數(shù)量與輸出文件的數(shù)量相關(guān)。如果reducer數(shù)太多,會(huì)產(chǎn)生大量小文件,對(duì)HDFS造成壓力。如果reducer數(shù)太少,每個(gè)reducer要處理很多數(shù)據(jù),容易拖慢運(yùn)行時(shí)間或者造成OOM。
合并小文件
-
輸入階段合并
需要更改Hive的輸入文件格式,即參數(shù)hive.input.format,默認(rèn)值是org.apache.hadoop.hive.ql.io.HiveInputFormat,我們改成org.apache.hadoop.hive.ql.io.CombineHiveInputFormat。
這樣比起上面調(diào)整mapper數(shù)時(shí),又會(huì)多出兩個(gè)參數(shù),分別是mapred.min.split.size.per.node和mapred.min.split.size.per.rack,含義是單節(jié)點(diǎn)和單機(jī)架上的最小split大小。如果發(fā)現(xiàn)有split大小小于這兩個(gè)值(默認(rèn)都是100MB),則會(huì)進(jìn)行合并。具體邏輯可以參看Hive源碼中的對(duì)應(yīng)類。 -
輸出階段合并
直接將hive.merge.mapfiles和hive.merge.mapredfiles都設(shè)為true即可,前者表示將map-only任務(wù)的輸出合并,后者表示將map-reduce任務(wù)的輸出合并。
另外,hive.merge.size.per.task可以指定每個(gè)task輸出后合并文件大小的期望值,hive.merge.size.smallfiles.avgsize可以指定所有輸出文件大小的均值閾值,默認(rèn)值都是1GB。如果平均大小不足的話,就會(huì)另外啟動(dòng)一個(gè)任務(wù)來(lái)進(jìn)行合并。
啟用壓縮
壓縮job的中間結(jié)果數(shù)據(jù)和輸出數(shù)據(jù),可以用少量CPU時(shí)間節(jié)省很多空間。壓縮方式一般選擇Snappy,效率最高。
要啟用中間壓縮,需要設(shè)定hive.exec.compress.intermediate為true,同時(shí)指定壓縮方式hive.intermediate.compression.codec為org.apache.hadoop.io.compress.SnappyCodec。另外,參數(shù)hive.intermediate.compression.type可以選擇對(duì)塊(BLOCK)還是記錄(RECORD)壓縮,BLOCK的壓縮率比較高。
輸出壓縮的配置基本相同,打開hive.exec.compress.output即可。
JVM重用
在MR job中,默認(rèn)是每執(zhí)行一個(gè)task就啟動(dòng)一個(gè)JVM。如果task非常小而碎,那么JVM啟動(dòng)和關(guān)閉的耗時(shí)就會(huì)很長(zhǎng)。可以通過(guò)調(diào)節(jié)參數(shù)mapred.job.reuse.jvm.num.tasks來(lái)重用。例如將這個(gè)參數(shù)設(shè)成5,那么就代表同一個(gè)MR job中順序執(zhí)行的5個(gè)task可以重復(fù)使用一個(gè)JVM,減少啟動(dòng)和關(guān)閉的開銷。但它對(duì)不同MR job中的task無(wú)效。
并行執(zhí)行與本地模式
-
并行執(zhí)行
Hive中互相沒(méi)有依賴關(guān)系的job間是可以并行執(zhí)行的,最典型的就是多個(gè)子查詢union all。在集群資源相對(duì)充足的情況下,可以開啟并行執(zhí)行,即將參數(shù)hive.exec.parallel設(shè)為true。另外hive.exec.parallel.thread.number可以設(shè)定并行執(zhí)行的線程數(shù),默認(rèn)為8,一般都?jí)蛴谩?/p> -
本地模式
Hive也可以不將任務(wù)提交到集群進(jìn)行運(yùn)算,而是直接在一臺(tái)節(jié)點(diǎn)上處理。因?yàn)橄颂峤坏郊旱膐verhead,所以比較適合數(shù)據(jù)量很小,且邏輯不復(fù)雜的任務(wù)。
設(shè)置hive.exec.mode.local.auto為true可以開啟本地模式。但任務(wù)的輸入數(shù)據(jù)總量必須小于hive.exec.mode.local.auto.inputbytes.max(默認(rèn)值128MB),且mapper數(shù)必須小于hive.exec.mode.local.auto.tasks.max(默認(rèn)值4),reducer數(shù)必須為0或1,才會(huì)真正用本地模式執(zhí)行。
嚴(yán)格模式
所謂嚴(yán)格模式,就是強(qiáng)制不允許用戶執(zhí)行3種有風(fēng)險(xiǎn)的HiveQL語(yǔ)句,一旦執(zhí)行會(huì)直接失敗。這3種語(yǔ)句是:
-
查詢分區(qū)表時(shí)不限定分區(qū)列的語(yǔ)句;
-
兩表join產(chǎn)生了笛卡爾積的語(yǔ)句;
-
用order by來(lái)排序但沒(méi)有指定limit的語(yǔ)句。
要開啟嚴(yán)格模式,需要將參數(shù)hive.mapred.mode設(shè)為strict。
采用合適的存儲(chǔ)格式
在HiveQL的create table語(yǔ)句中,可以使用stored as ...指定表的存儲(chǔ)格式。Hive表支持的存儲(chǔ)格式有TextFile、SequenceFile、RCFile、Avro、ORC、Parquet等。
存儲(chǔ)格式一般需要根據(jù)業(yè)務(wù)進(jìn)行選擇,在我們的實(shí)操中,絕大多數(shù)表都采用TextFile與Parquet兩種存儲(chǔ)格式之一。
TextFile是最簡(jiǎn)單的存儲(chǔ)格式,它是純文本記錄,也是Hive的默認(rèn)格式。雖然它的磁盤開銷比較大,查詢效率也低,但它更多地是作為跳板來(lái)使用。RCFile、ORC、Parquet等格式的表都不能由文件直接導(dǎo)入數(shù)據(jù),必須由TextFile來(lái)做中轉(zhuǎn)。
Parquet和ORC都是Apache旗下的開源列式存儲(chǔ)格式。列式存儲(chǔ)比起傳統(tǒng)的行式存儲(chǔ)更適合批量OLAP查詢,并且也支持更好的壓縮和編碼。我們選擇Parquet的原因主要是它支持Impala查詢引擎,并且我們對(duì)update、delete和事務(wù)性操作需求很低。
這里就不展開講它們的細(xì)節(jié),可以參考各自的官網(wǎng):
https://parquet.apache.org/
https://orc.apache.org/
總結(jié)
以上是生活随笔為你收集整理的HiveSQL常用优化方法全面总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 水晶先锋出装 LOL斯卡纳神装推荐
- 下一篇: 实战 | 利用Delta Lake使Sp