深入淺出資料倉儲中SQL效能優化之Hive篇

CSDN發表於2015-01-14

一個Hive查詢生成多個Map Reduce Job,一個Map Reduce Job又有Map,Reduce,Spill,Shuffle,Sort等多個階段,所以針對Hive查詢的優化可以大致分為針對MR中單個步驟的優化(其中又會有細分),針對MR全域性的優化,和針對整個查詢(多MRJob)的優化,下文會分別闡述。

在開始之前,先把MR的流程圖帖出來(摘自Hadoop權威指南),方便後面對照。另外要說明的是,這個優化只是針對Hive 0.9版本,而不是後來Hortonwork發起Stinger專案之後的版本。相對應的Hadoop版本是1.x而非2.x。

Map階段的優化(Map phase)

Map階段的優化,主要是確定合適的Map數。那麼首先要了解Map數的計算公式:

num_Map_tasks = max[${Mapred.min.split.size},
                min(${dfs.block.size}, ${Mapred.max.split.size})]
  • Mapred.min.split.size指的是資料的最小分割單元大小。
  • Mapred.max.split.size指的是資料的最大分割單元大小。
  • dfs.block.size指的是HDFS設定的資料塊大小。

一般來說dfs.block.size這個值是一個已經指定好的值,而且這個引數Hive是識別不到的:

Hive> set dfs.block.size;
dfs.block.size is undefined

所以實際上只有Mapred.min.split.size和Mapred.max.split.size這兩個引數(本節內容後面就以min和max指代這兩個引數)來決定Map數量。在Hive中min的預設值是1B,max的預設值是256MB:

Hive> set Mapred.min.split。size;
Mapred.min.split.size=1
Hive> set Mapred.max.split。size;
Mapred.max.split.size=256000000

所以如果不做修改的話,就是1個Map task處理256MB資料,我們就以調整max為主。通過調整max可以起到調整Map數的作用,減小max可以增加Map數,增大max可以減少Map數。需要提醒的是,直接調整Mapred.Map.tasks這個引數是沒有效果的。

調整大小的時機根據查詢的不同而不同,總的來講可以通過觀察Map task的完成時間來確定是否需要增加Map資源。如果Map task的完成時間都是接近1分鐘,甚至幾分鐘了,那麼往往增加Map數量,使得每個Maptask處理的資料量減少,能夠讓Map task更快完成;而如果Map task的執行時間已經很少了,比如10-20秒,這個時候增加Map不太可能讓Maptask更快完成,反而可能因為Map需要的初始化時間反而讓Job總體速度變慢,這個時候反而需要考慮是否可以把Map的數量減少,這樣可以節省更多資源給其他Job。

Reduce階段的優化(Reduce phase)

這裡說的Reduce階段,是指前面流程圖中的Reduce phase(實際的Reduce計算)而非圖中整個Reduce task。Reduce階段優化的主要工作也是選擇合適的Reducetask數量,跟上面的Map優化類似。

與Map優化不同的是,Reduce優化時,可以直接設定Mapred。Reduce。tasks引數從而直接指定Reduce的個數。當然直接指定Reduce個數雖然比較方便,但是不利於自動擴充套件。Reduce數的設定雖然相較Map更靈活,但是也可以像Map一樣設定一個自動生成規則,這樣執行定時Job的時候就不用擔心原來設定的固定Reduce數會由於資料量的變化而不合適。

Hive估算Reduce數量的時候,使用的是下面的公式:

num_Reduce_tasks = min[${Hive.exec.Reducers.max}, 
                      (${input.size} / ${ Hive.exec.Reducers.bytes.per.Reducer})]

也就是說,根據輸入的資料量大小來決定Reduce的個數,預設Hive.exec.Reducers.bytes.per.Reducer為1G,而且Reduce個數不能超過一個上限引數值,這個引數的預設取值為999。所以我們可以調整Hive.exec.Reducers.bytes.per.Reducer來設定Reduce個數。

設定Reduce數同樣也是根據執行時間作為參考調整,並且可以根據特定的業務需求、工作負載型別總結出經驗,所以不再贅述。

Map與Reduce之間的優化(Spill, copy, Sort phase)

Map phase和Reduce phase之間主要有3道工序。首先要把Map輸出的結果進行排序後做成中間檔案,其次這個中間檔案就能分發到各個Reduce,最後Reduce端在執行Reducephase之前把收集到的排序子檔案合併成一個排序檔案。這個部分可以調的引數挺多,但是一般都是不要調整的,不必重點關注。

Spill 與 Sort

在Spill階段,由於記憶體不夠,資料可能沒辦法在記憶體中一次性排序完成,那麼就只能把區域性排序的檔案先儲存到磁碟上,這個動作叫Spill,然後Spill出來的多個檔案可以在最後進行merge。如果發生Spill,可以通過設定io.Sort.mb來增大Mapper輸出buffer的大小,避免Spill的發生。另外合併時可以通過設定io.Sort.factor來使得一次效能夠合併更多的資料。除錯引數的時候,一個要看Spill的時間成本,一個要看merge的時間成本,還需要注意不要撐爆記憶體(io.Sort.mb是算在Map的記憶體裡面的)。Reduce端的merge也是一樣可以用io.Sort.factor。一般情況下這兩個引數很少需要調整,除非很明確知道這個地方是瓶頸。

Copy

copy階段是把檔案從Map端copy到Reduce端。預設情況下在5%的Map完成的情況下Reduce就開始啟動copy,這個有時候是很浪費資源的,因為Reduce一旦啟動就被佔用,一直等到Map全部完成,收集到所有資料才可以進行後面的動作,所以我們可以等比較多的Map完成之後再啟動Reduce流程,這個比例可以通Mapred.Reduce.slowstart.completed.Maps去調整,他的預設值就是5%。如果覺得這麼做會減慢Reduce端copy的進度,可以把copy過程的執行緒增大。tasktracker.http.threads可以決定作為server端的Map用於提供資料傳輸服務的執行緒,Mapred.Reduce.parallel.copies可以決定作為client端的Reduce同時從Map端拉取資料的並行度(一次同時從多少個Map拉資料),修改引數的時候這兩個注意協調一下,server端能處理client端的請求即可。

檔案格式的優化

檔案格式方面有兩個問題,一個是給輸入和輸出選擇合適的檔案格式,另一個則是小檔案問題。小檔案問題在目前的Hive環境下已經得到了比較好的解決,Hive的預設配置中就可以在小檔案輸入時自動把多個檔案合併給1個Map處理,輸出時如果檔案很小也會進行一輪單獨的合併,所以這裡就不專門討論了。相關的引數可以在這裡找到。

關於檔案格式,Hive0.9版本有3種,textfile,sequencefile和rcfile。總體上來說,rcfile的壓縮比例和查詢時間稍好一點,所以推薦使用。

關於使用方法,可以在建表結構時可以指定格式,然後指定壓縮插入:

create table rc_file_test( col int ) stored as rcfile;
set Hive.exec.compress.output = true;
insert overwrite table rc_file_test
select * from source_table;

另外時也可以指定輸出格式,也可以通過Hive。default。fileformat來設定輸出格式,適用於create table as select的情況:

set Hive.default.fileformat = SequenceFile;
set Hive.exec.compress.output = true; 
/*對於sequencefile,有record和block兩種壓縮方式可選,block壓縮比更高*/
set Mapred.output.compression.type = BLOCK; 
create table seq_file_test
as select * from source_table;

上面的檔案格式轉換,其實是由Hive完成的(也就是插入動作)。但是也可以由外部直接匯入純文字(可以按照這裡的做法預先壓縮),或者是由MapReduceJob生成的資料。

值得注意的是,Hive讀取sequencefile的時候,是把key忽略的,也就是直接讀value並且按照指定分隔符分隔欄位。但是如果Hive的資料來源是從mr生成的,那麼寫sequencefile的時候,key和value都是有意義的,key不能被忽略,而是應該當成第一個欄位。為了解決這種不匹配的情況,有兩種辦法。一種是要求凡是結果會給Hive用的mrJob輸出value的時候帶上key。但是這樣的話對於開發是一個負擔,讀寫資料的時候都要注意這個情況。所以更好的方法是第二種,也就是把這個源自於Hive的問題交給Hive解決,寫一個InputFormat包裝一下,把value輸出加上key即可。以下是核心程式碼,修改了RecordReader的next方法:

public synchronized boolean next(K key, V value) throws IOException 
{
    Text tKey = (Text) key;
    Text tValue = (Text) value;
    if (!super.next(innerKey, innerValue)) 
        return false;

    Text inner_key = (Text) innerKey; //在建構函式中用createKey()生成
    Text inner_value = (Text) innerValue; //在建構函式中用createValue()生成

    tKey.set(inner_key);
    tValue.set(inner_key.toString() + '\t' + inner_value.toString()); // 分隔符注意自己定義
    return true;
}

Job整體優化

有一些問題必須從Job的整體角度去觀察。這裡討論幾個問題:Job執行模式(本地執行v.s.分散式執行)、JVM重用、索引、Join演算法、資料傾斜。

Job執行模式

Hadoop的Map Reduce Job可以有3種模式執行,即本地模式,偽分散式,還有真正的分散式。本地模式和偽分散式都是在最初學習Hadoop的時候往往被說成是做單機開發的時候用到。但是實際上對於處理資料量非常小的Job,直接啟動分散式Job會消耗大量資源,而真正執行計算的時間反而非常少。這個時候就應該使用本地模式執行mrJob,這樣執行的時候不會啟動分散式Job,執行速度就會快很多。比如一般來說啟動分散式Job,無論多小的資料量,執行時間一般不會少於20s,而使用本地mr模式,10秒左右就能出結果。

設定執行模式的主要引數有三個,一個是Hive.exec.mode.local.auto,把他設為true就能夠自動開啟local mr模式。但是這還不足以啟動localmr,輸入的檔案數量和資料量大小必須要控制,這兩個引數分別為Hive.exec.mode.local.auto.tasks.max和Hive.exec.mode.local.auto.inputbytes.max,預設值分別為4和128MB,即預設情況下,Map處理的檔案數不超過4個並且總大小小於128MB就啟用localmr模式。

JVM重用

正常情況下,MapReduce啟動的JVM在完成一個task之後就退出了,但是如果任務花費時間很短,又要多次啟動JVM的情況下(比如對很大資料量進行計數操作),JVM的啟動時間就會變成一個比較大的overhead。在這種情況下,可以使用jvm重用的引數:

set Mapred.Job.reuse.jvm.num.tasks = 5;

他的作用是讓一個jvm執行多次任務之後再退出。這樣一來也能節約不少JVM啟動時間。

索引

總體上來說,Hive的索引目前還是一個不太適合使用的東西,這裡只是考慮到敘述完整性,對其進行基本的介紹。

Hive中的索引架構開放了一個介面,允許你根據這個介面去實現自己的索引。目前Hive自己有一個參考的索引實現(CompactIndex),後來在0.8版本中又加入點陣圖索引。這裡就講講CompactIndex。

CompactIndex的實現原理類似一個lookup table,而非傳統資料庫中的B樹。如果你對table A的col1做了索引,索引檔案本身就是一個table,這個table會有3列,分別是col1的列舉值,每個值對應的資料檔案位置,以及在這個檔案位置中的偏移量。通過這種方式,可以減少你查詢的資料量(偏移量可以告訴你從哪個位置開始找,自然只需要定位到相應的block),起到減少資源消耗的作用。但是就其效能來說,並沒有很大的改善,很可能還不如構建索引需要花的時間。所以在叢集資源充足的情況下,沒有太大必要考慮索引。

CompactIndex的還有一個缺點就是使用起來不友好,索引建完之後,使用之前還需要根據查詢條件做一個同樣剪裁才能使用,索引的內部結構完全暴露,而且還要花費額外的時間。具體看看下面的使用方法就瞭解了:

/*在index_test_table表的id欄位上建立索引*/
create index idx on table index_test_table(id)  
as 'org.apache.Hadoop.Hive.ql.index.compact.CompactIndexHandler' with deferred rebuild;
alter index idx on index_test_table rebuild;

/*索引的剪裁。找到上面建的索引表,根據你最終要用的查詢條件剪裁一下。*/
/*如果你想跟RDBMS一樣建完索引就用,那是不行的,會直接報錯,這也是其麻煩的地方*/
create table my_index
as select _bucketname, `_offsets`
from default__index_test_table_idx__ where id = 10;

/*現在可以用索引了,注意最終查詢條件跟上面的剪裁條件一致*/
set Hive.index.compact.file = /user/Hive/warehouse/my_index; 
set Hive.input.format = org.apache.Hadoop.Hive.ql.index.compact.HiveCompactIndexInputFormat;
select count(*) from index_test_table where id = 10;

Join演算法

處理分散式join,一般有兩種方法:

  • replication join:把其中一個表複製到所有節點,這樣另一個表在每個節點上面的分片就可以跟這個完整的表join了;
  • repartition join:把兩份資料按照join key進行hash重分佈,讓每個節點處理hash值相同的join key資料,也就是做區域性的join。

這兩種方式在M/R Job中分別對應了Map side join和Reduce side join。在一些MPP DB中,資料可以按照某列欄位預先進行hash分佈,這樣在跟這個表以這個欄位為joinkey進行join的時候,該表肯定不需要做資料重分佈了,這種功能是以HDFS作為底層檔案系統的Hive所沒有的。

在預設情況下,Hive的join策略是進行Reduce side join。當兩個表中有一個是小表的時候,就可以考慮用Map join了,因為小表複製的代價會好過大表Shuffle的代價。使用Mapjoin的配置方法有兩種,一種直接在sql中寫hint,語法是/*+MapJOIN (tbl)*/,其中tbl就是你想要做replication的表。另一種方法是設定Hive.auto.convert.join= true,這樣Hive會自動判斷當前的join操作是否合適做Map join,主要是找join的兩個表中有沒有小表。至於多大的表算小表,則是由Hive.smalltable.filesize決定,預設25MB。

但是有的時候,沒有一個表足夠小到能夠放進記憶體,但是還是想用Map join怎麼辦?這個時候就要用到bucket Map join。其方法是兩個join表在joinkey上都做hash bucket,並且把你打算複製的那個(相對)小表的bucket數設定為大表的倍數。這樣資料就會按照join key做hashbucket。小表依然複製到所有節點,Map join的時候,小表的每一組bucket載入成hashtable,與對應的一個大表bucket做區域性join,這樣每次只需要載入部分hashtable就可以了。

然後在兩個表的join key都具有唯一性的時候(也就是可做主鍵),還可以進一步做Sort merge bucket Map join。做法還是兩邊要做hashbucket,而且每個bucket內部要進行排序。這樣一來當兩邊bucket要做區域性join的時候,只需要用類似merge Sort演算法中的merge操作一樣把兩個bucket順序遍歷一遍即可完成,這樣甚至都不用把一個bucket完整的載入成hashtable,這對效能的提升會有很大幫助。

然後這裡以一個完整的實驗說明這幾種join演算法如何操作。

首先建表要帶上bucket:

create table Map_join_test(id int)
clustered by (id) Sorted by (id) into 32 buckets
stored as textfile;

然後插入我們準備好的800萬行資料,注意要強制劃分成bucket(也就是用Reduce劃分hash值相同的資料到相同的檔案):

set Hive.enforce.bucketing = true;
insert overwrite table Map_join_test
select * from Map_join_source_data;

這樣這個表就有了800萬id值(且裡面沒有重複值,所以可以做Sort merge),佔用80MB左右。

接下來我們就可以一一嘗試Map join的演算法了。首先是普通的Map join:

select /*+Mapjoin(a) */count(*)
from Map_join_test a
join Map_join_test b on a.id = b.id;

然後就會看到分發hash table的過程:

2013-08-31 09:08:43     Starting to launch local task to process Map join;      maximum memory = 1004929024
2013-08-31 09:08:45     Processing rows:   200000  Hashtable size: 199999  Memory usage:   38823016        rate:   0.039
2013-08-31 09:08:46     Processing rows:   300000  Hashtable size: 299999  Memory usage:   56166968        rate:   0.056
……
2013-08-31 09:12:39     Processing rows:  4900000 Hashtable size: 4899999 Memory usage:   896968104       rate:   0.893
2013-08-31 09:12:47     Processing rows:  5000000 Hashtable size: 4999999 Memory usage:   922733048       rate:   0.918
Execution failed with exit status: 2
Obtaining error information

Task failed!
Task ID:
  Stage-4

不幸的是,居然記憶體不夠了,直接做Map join失敗了。但是80MB的大小為何用1G的heap size都放不下?觀察整個過程就會發現,平均一條記錄需要用到200位元組的儲存空間,這個overhead太大了,對於Mapjoin的小表size一定要好好評估,如果有幾十萬記錄數就要小心了。雖然不太清楚其中的構造原理,但是在網際網路上也能找到其他的例證,比如這裡和這裡,平均一行500位元組左右。這個明顯比一般的表一行佔用的資料量要大。不過Hive也在做這方面的改進,爭取縮小hash
table,比如Hive-6430。

所以接下來我們就用bucket Map join,之前分的bucket就派上用處了。只需要在上述sql的前面加上如下的設定:

set Hive。optimize。bucketMapjoin = true;

然後還是會看到hash table分發:

2013-08-31 09:20:39     Starting to launch local task to process Map join;      maximum memory = 1004929024
2013-08-31 09:20:41     Processing rows:   200000  Hashtable size: 199999  Memory usage:   38844832        rate:   0.039
2013-08-31 09:20:42     Processing rows:   275567  Hashtable size: 275567  Memory usage:   51873632        rate:   0.052
2013-08-31 09:20:42     Dump the hashtable into file: file:/tmp/Hadoop/Hive_2013-08-31_21-20-37_444_1135806892100127714/-local-10003/HashTable-Stage-1/MapJoin-a-10-000000_0。hashtable
2013-08-31 09:20:46     Upload 1 File to: file:/tmp/Hadoop/Hive_2013-08-31_21-20-37_444_1135806892100127714/-local-10003/HashTable-Stage-1/MapJoin-a-10-000000_0。hashtable File size: 11022975
2013-08-31 09:20:47     Processing rows:   300000  Hashtable size: 24432   Memory usage:   8470976 rate:   0.008
2013-08-31 09:20:47     Processing rows:   400000  Hashtable size: 124432  Memory usage:   25368080        rate:   0.025
2013-08-31 09:20:48     Processing rows:   500000  Hashtable size: 224432  Memory usage:   42968080        rate:   0.043
2013-08-31 09:20:49     Processing rows:   551527  Hashtable size: 275960  Memory usage:   52022488        rate:   0.052
2013-08-31 09:20:49     Dump the hashtable into file: file:/tmp/Hadoop/Hive_2013-08-31_21-20-37_444_1135806892100127714/-local-10003/HashTable-Stage-1/MapJoin-a-10-000001_0。hashtable
……

這次就會看到每次構建完一個hash table(也就是所對應的對應一個bucket),會把這個hash table寫入檔案,重新構建新的hashtable。這樣一來由於每個hash table的量比較小,也就不會有記憶體不足的問題,整個sql也能成功執行。不過光光是這個複製動作就要花去3分半的時間,所以如果整個Job本來就花不了多少時間的,那這個時間就不可小視。

最後我們試試Sort merge bucket Map join,在bucket Map join的基礎上加上下面的設定即可:

set Hive.optimize.bucketMapjoin.Sortedmerge = true;
set Hive.input.format = org.apache.Hadoop.Hive.ql.io.BucketizedHiveInputFormat;

Sort merge bucket Map join是不會產生hash table複製的步驟的,直接開始做實際Map端join操作了,資料在join的時候邊做邊讀。跳過複製的步驟,外加join演算法的改進,使得Sortmerge bucket Map join的效率要明顯好於bucket Map join。

關於join的演算法雖然有這麼些選擇,但是個人覺得,對於日常使用,掌握預設的Reduce join和普通的(無bucket)Map join已經能解決大多數問題。如果小表不能完全放記憶體,但是小表相對大表的size量級差別也非常大的時候也可以試試bucketMap join,不過其hash table分發的過程會浪費不少時間,需要評估下是否能夠比Reduce join更高效。而Sort merge
bucket Map join雖然效能不錯,但是把資料做成bucket本身也需要時間,另外其發動條件比較特殊,就是兩邊join key必須都唯一(很多介紹資料中都不提這一點。強調下必須都是唯一,哪怕只有一個表不唯一,出來的結果也是錯的。當然,其實這點完全可以根據其演算法原理推敲出來)。這樣的場景相對比較少見,“使用者基本表join 使用者擴充套件表”以及“使用者今天的資料快照 join 使用者昨天的資料快照”這類場景可能比較合適。

這裡順便說個題外話,在資料倉儲中,小表往往是維度表,而小表Map join這件事情其實用udf代替還會更快,因為不用單獨啟動一輪Job,所以這也是一種可選方案。當然前提條件是維度表是固定的自然屬性(比如日期),只增加不修改(比如網站的頁面編號)的情況也可以考慮。如果維度有更新,要做緩慢變化維的,當然還是維表好維護。至於維表原本的一個主要用途OLAP,以Hive目前的效能是沒法實現的,也就不需要多慮了。

資料傾斜

所謂資料傾斜,說的是由於資料分佈不均勻,個別值集中佔據大部分資料量,加上Hadoop的計算模式,導致計算資源不均勻引起效能下降。下圖就是一個例子:

還是拿網站的訪問日誌說事吧。假設網站訪問日誌中會記錄使用者的user_id,並且對於註冊使用者使用其使用者表的user_id,對於非註冊使用者使用一個user_id=0代表。那麼鑑於大多數使用者是非註冊使用者(只看不寫),所以user_id=0佔據了絕大多數。而如果進行計算的時候如果以user_id作為groupby的維度或者是join key,那麼個別Reduce會收到比其他Reduce多得多的資料——因為它要接收所有user_id=0的記錄進行處理,使得其處理效果會非常差,其他Reduce都跑完很久了它還在執行。

傾斜分成group by造成的傾斜和join造成的傾斜,需要分開看。

group by造成的傾斜有兩個引數可以解決,一個是Hive.Map.aggr,預設值已經為true,意思是會做Map端的combiner。所以如果你的group
by查詢只是做count(*)的話,其實是看不出傾斜效果的,但是如果你做的是count(distinct),那麼還是會看出一點傾斜效果。另一個引數是Hive.groupby.skewindata。這個引數的意思是做Reduce操作的時候,拿到的key並不是所有相同值給同一個Reduce,而是隨機分發,然後Reduce做聚合,做完之後再做一輪MR,拿前面聚合過的資料再算結果。所以這個引數其實跟Hive.Map.aggr做的是類似的事情,只是拿到Reduce端來做,而且要額外啟動一輪Job,所以其實不怎麼推薦用,效果不明顯。

如果說要改寫SQL來優化的話,可以按照下面這麼做:

/*改寫前*/
select a, count(distinct b) as c from tbl group by a;
/*改寫後*/
select a, count(*) as c
from (select distinct a, b from tbl) group by a;

join造成的傾斜,就比如上面描述的網站訪問日誌和使用者表兩個表join:

select a.* from logs a join users b on a。user_id = b.user_id;

Hive給出的解決方案叫skew join,其原理把這種user_id = 0的特殊值先不在Reduce端計算掉,而是先寫入hdfs,然後啟動一輪Mapjoin專門做這個特殊值的計算,期望能提高計算這部分值的處理速度。當然你要告訴Hive這個join是個skew join,即:

set Hive.optimize.skewjoin = true;

還有要告訴Hive如何判斷特殊值,根據Hive.skewjoin.key設定的數量Hive可以知道,比如預設值是100000,那麼超過100000條記錄的值就是特殊值。

skew join的流程可以用下圖描述:

另外對於特殊值的處理往往跟業務有關係,所以也可以從業務角度重寫sql解決。比如前面這種傾斜join,可以把特殊值隔離開來(從業務角度說,users表應該不存在user_id= 0的情況,但是這裡還是假設有這個值,使得這個寫法更加具有通用性):

select a.* from 
(
select a.*
from (select * from logs where user_id = 0)  a 
join (select * from users where user_id = 0) b 
on a。user_id =  b。user_id
union all
select a.* 
from logs a join users b
on a。user_id <> 0 and a。user_id = b.user_id
)t;

資料傾斜不僅僅是Hive的問題,其實是share nothing架構下必然會碰到的資料分佈問題,對此學界也有專門的研究,比如skewtune。

SQL整體優化

前面對於單個Job如何做優化已經做過詳細討論,但是Hive查詢會生成多個Job,針對多個Job,有什麼地方需要優化?

Job間並行

首先,在Hive生成的多個Job中,在有些情況下Job之間是可以並行的,典型的就是子查詢。當需要執行多個子查詢union all或者join操作的時候,Job間並行就可以使用了。比如下面的程式碼就是一個可以並行的場景示意:

select * from 
(
   select count(*) from logs 
   where log_date = 20130801 and item_id = 1
   union all 
   select count(*) from logs 
   where log_date = 20130802 and item_id = 2
   union all 
   select count(*) from logs 
   where log_date = 20130803 and item_id = 3
)t

設定Job間並行的引數是Hive.exec.parallel,將其設為true即可。預設的並行度為8,也就是最多允許sql中8個Job並行。如果想要更高的並行度,可以通過Hive.exec.parallel.thread.number引數進行設定,但要避免設定過大而佔用過多資源。

減少Job數

另外在實際開發過程中也發現,一些實現思路會導致生成多餘的Job而顯得不夠高效。比如這個需求:查詢某網站日誌中訪問過頁面a和頁面b的使用者數量。低效的思路是面向明細的,先取出看過頁面a的使用者,再取出看過頁面b的使用者,然後取交集,程式碼如下:

select count(*) 
from 
(select distinct user_id 
from logs where page_name = ‘a’) a
join 
(select distinct user_id 
from logs where blog_owner = ‘b’) b 
on a.user_id = b.user_id;

這樣一來,就要產生2個求子查詢的Job,一個用於關聯的Job,還有一個計數的Job,一共有4個Job。

但是我們直接用面向統計的方法去計算的話(也就是用group by替代join),則會更加符合M/R的模式,而且生成了一個完全不帶子查詢的sql,只需要用一個Job就能跑完:

select count(*) 
from logs group by user_id
having (count(case when page_name = ‘a’ then 1 end) > 0
    and count(case when page_name = ‘b’ then 1 end) > 0)

第一種查詢方法符合思考問題的直覺,是工程師和分析師在實際查資料中最先想到的寫法,但是如果在目前Hive的query planner不是那麼智慧的情況下,想要更加快速的跑出結果,懂一點工具的內部機理也是必須的。

相關文章