Hive.分組排序和TOP

       HQL作爲類SQL的查詢分析語言,到目前爲止,應該也還未能達到其它流行的SQL(如Transact-SQL, MySQL)實現那樣完善。而在公司的生產環境中,我想應該也不會緊貼Hive版本更新的步伐,始終部署最新版的Hive;可能會滯後一兩個大版本神馬的;畢竟,雖然開源工具的透明性是一大利好,但與閉源的商業工具相比,在可用性等問題上的保障性還是略弱。

       使用HQL進行離線分析用戶數據時,就算已經過聚合處理,但我們也可能只對那些突出的量化指標或者這些指標的增量變化感興趣,所以對聚合數據排序(按某列降序?增序?)成爲很基本的需要,這在HQL這樣尚未成熟的語言中,結合orderby, limit子句可以毫無鴨梨地完成。

       然而,即使我們可以把多個字段放入order by子句中,並指定各個字段的升降順序,如:

order by fieldA desc, fieldB [asc], fieldC desc

       但排序操作始終是全局的,我們有時候想要的卻是分組排序,即按fieldA排序以後,然後針對fieldA的每個值所對應的fieldB和(或)fieldC排序,而不是像order by那樣,針對所有fieldA的值對fieldB和(或)fieldC排序。

       爲了滿足這個需要,Transact-SQL提供了over, partition by句和 row_number()函數,而Hive也在0.11中引入over, partition by子句和rank函數,以此提供方便的窗口分析(分組分析)功能。

       那對於0.11版之前的Hive,我們可以實現分組排序嗎?答案是肯定的,只是看起來沒那麼直接。

       要實現這個需求,就需要請出distribute by, sort by這兩個重要角色了,distribute by能夠執行我們需要的分組功能,再結合Hive查詢的MapReduce Job特性,sort by又可以在分組內進行局部排序。

       當然,如果只有它們,我們只能得到排序後的一堆數據,但是無法知道每一條數據的名次,這就要自己編寫UDF函數,來確定和返回名次了,這個函數貌似在網絡上流傳甚廣:

public final class Rank extends UDF {
      private int counter;
      private String last_key ="";
 
      public int evaluate(final String key) {
             if (key == null) {
                    this.last_key= "";
                    this.counter= 0;
                    return counter;
             }
             if(!key.equalsIgnoreCase(this.last_key)) {
                    this.counter= 0;
                    this.last_key= key;
             }
             return this.counter++;
      }
}

       在這裏我們忽略了自定義UDF的註冊的環節。。。在分組之後,應用Rank函數,這個函數始終跟蹤最新的參數值,在參數值連續相同的情況下,就將字段counter作自增操作並返回這個計數值;而如果出現和上一次函數調用不同的參數值,Rank函數會重置其計數值字段和key字段(對應參數值)使我們得到一個int類型的名次值。

       Hive裏稱這個爲自定義函數,實際上每個自定義函數是一個繼承了UDF類,並提供和實現了evaluate方法的類;這個叫法略不福啊。

       有了distribute by, sort by和這個Rank函數,我們就能夠實現分組排序了,編寫HQL查詢腳本之前,我們還需要明確:

       1. 分組字段:distribute by的字段是哪個(些)?

       2. 排序依據:sort by的字段是哪個(些)?

       3. 函數參數:Rank函數需要String參數,我們應該給Rank函數傳遞什麼東西作爲實參?


       不曉得是因爲以上這3個問題確實像我們在教科書上偶爾會看到的“容易證得”,還是因爲博主們只是想了這個問題,並沒有實踐,反正我看到的網絡上講分組排序(TOP)的博文都沒有明確地提出這3個問題。而按我的實踐經歷來看,初次實現這種需求的童鞋,就算看了這些博文,在得到正確結果之前,應該都會經歷各種困惑,下面我們從實際的場景來看看。

       比如,我們需要查詢得到這樣的數據:每日使用應用myAPP的UV量TOP 30的設備,和這TOP 10的設備中每個設備的流量(VV)最高的10項版塊內容(以內容ID來區分);

       假定查詢所需的Hive表爲:hiveTab_useraction 

       思路梗概:先用一個子查詢查出來每臺設備的訪問內容,同時,用一個子查詢查出來TOP30的設備,然後兩個表做內連接(join),然後在外層查詢中提取所需字段列和數據列。

       在這個流程裏面:

       1)找出TOP10的設備這個環節看起來沒有涉及分組排序,但還是需要考慮上面3個問題,因爲我們要得到名次,而order by貌似不能同Rank函數友好協作(也有可能是我使用的方式不科學呢),而且,在以下呈現的腳本中,我們還生成了一個常量字符串的distribute_key; 

       2)然後在外層循環中更需要考慮上面3個問題。

       請見HQL腳本:

select
	device_rank,
	device_info,
	vv_rank,
	pageID,	
	act_vv
from
(
	select
		device_rank,
		device_info,
		(Rank(device_rank) + 1) as vv_rank,
		pageID,
		act_vv
	from
	(	
		select
			t2.device_rank,
			t2.device_info,
			t1.pageID,
			t1.act_vv
		from
		(
			select
				fieldA as device_info,
				pageID,
				count(1) as act_vv
			from hiveTab_useraction
			where `date` >= dateStart and `date` <= dateEnd
			group by fieldA, pageID
		) t1
		join
		(
			select
				(Rank(distribute_key) + 1) as device_rank,
				device_info,
				act_uv
			from
			(
				select
					distribute_key,
					device_info,
					act_uv
				from
				(
					select
						'topdevice' as distribute_key,
						device_info,
						act_uv
					from
					(
						select
							fieldA as device_info,
							count(distinct uid) as act_uv
						from hiveTab_useraction
						where `date` >= dateStart and `date` <= dateEnd
						group by fieldA
					) t
					order by act_uv desc
					limit 10
				) t
				distribute by distribute_key 
				sort by act_uv desc
			) t
		) t2 on (t1.device_info = t2.device_info)
		distribute by t2.device_rank
		sort by t2.device_rank, t1.act_vv desc
	) t
) t
where vv_rank <= 10

       從腳本實測來看,上面提到的需要明確的3個問題,真的很重要。另外,Rank函數返回的名次是從0開始,所以我們需要作+1處理。

       Hive向普通用戶也開放了自行編寫、註冊和使用自定義函數的功能,這一點確實帶來了很大的擴展性。


參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章