COUNT(DISTINCT xxx)
在hive中很容易造成數(shù)據(jù)傾斜。針對這一情況窄赋,網(wǎng)上已有很多優(yōu)化方法哟冬,這里不再贅述。
但有時忆绰,“數(shù)據(jù)傾斜”又幾乎是必然的柒傻。我們來舉個例子:
假設(shè)表detail_sdk_session中記錄了訪問某網(wǎng)站M的客戶端會話信息,即:如果用戶A打開app客戶端较木,則會產(chǎn)生一條會話信息記錄在該表中红符,該表的粒度為“一次”會話,其中每次會話都記錄了用戶的唯一標示uuid,uuid是一個很長的字符串预侯,假定其長度為64位≈驴現(xiàn)在的需求是:每天統(tǒng)計當月的活用用戶數(shù)——“月活躍用戶數(shù)”(當月訪問過app就為活躍用戶)。我們以2016年1月為例進行說明萎馅,now表示當前日期双戳。
最簡單的方法
這個問題邏輯上很簡單,SQL也很容易寫出來糜芳,例如:
SELECT
COUNT(DISTINCT uuid)
FROM detail_sdk_session t
WHERE t.date >= '2016-01-01' AND t.date <= now
上述SQL代碼中飒货,now表示當天的日期。很容易想到峭竣,越接近月末塘辅,上面的統(tǒng)計的數(shù)據(jù)量就會越大。更重要的是皆撩,在這種情況下扣墩,“數(shù)據(jù)傾斜”是必然的,因為只有一個reducer在進行COUNT(DISTINCT uuid)的計算扛吞,所有的數(shù)據(jù)都流向唯一的一個reducer呻惕,不傾斜才怪。
優(yōu)化1
其實滥比,在COUNT(DISTINCT xxx)的時候亚脆,我們可以采用“分治”的思想來解決。對于上面的例子盲泛,首先我們按照uuid的前n位進行GROUP BY濒持,并做COUNT(DISTINCT )操作,然后再對所有的COUNT(DISTINCT)結(jié)果進行求和查乒。
我們先把SQL寫出來弥喉,然后再做分析。
-- 外層SELECT求和
SELECT
SUM(mau_part) mau
FROM
(
-- 內(nèi)層SELECT分別進行COUNT(DISTINCT)計算
SELECT
substr(uuid, 1, 3) uuid_part,
COUNT(DISTINCT substr(uuid, 4)) AS mau_part
FROM detail_sdk_session
WHERE partition_date >= '2016-01-01' AND partition_date <= now
GROUP BY substr(uuid, 1, 3)
) t;
上述SQL中玛迄,內(nèi)層SELECT根據(jù)uuid的前3位進行GROUP BY由境,并計算相應(yīng)的活躍用戶數(shù)COUNT(DISTINCT),外層SELECT求和蓖议,得到最終的月活躍用戶數(shù)虏杰。
這種方法的好處在于,在不同的reducer各自進行COUNT(DISTINCT)計算勒虾,充分發(fā)揮hadoop的優(yōu)勢纺阔,然后進行求和。
注意修然,上面SQL中笛钝,n設(shè)為3质况,不應(yīng)過大。
為什么n不應(yīng)該太大呢玻靡?
我們假定uuid是由字母和數(shù)字組成的:大寫字母结榄、小寫字母和數(shù)字,字符總數(shù)為26+26+10=62囤捻。
理論上臼朗,內(nèi)層SELECT進行GROUP BY時,會有 62^n 個分組蝎土,外層SELECT就會進行 62^n 次求和视哑。所以n不宜過大。
當然誊涯,如果數(shù)據(jù)量十分巨大挡毅,n必須充分大,才能保證內(nèi)層SELECT中的COUNT(DISTINCT)能夠計算出來醋拧,此時可以再嵌套一層SELECT慷嗜,這里不再贅述淀弹。
優(yōu)化2
其實丹壕,很多博客中都記錄了使用GROUP BY
操作代替 COUNT(DISTINCT)
操作抵赢,但有時僅僅使用GROUP BY操作還不夠焕窝,還需要加點小技巧。
還是先來看一下代碼:
-- 第三層SELECT
SELECT
SUM(s.mau_part) mau
FROM
(
-- 第二層SELECT
SELECT
tag,
COUNT(*) mau_part
FROM
(
-- 第一層SELECT
SELECT
uuid,
CAST(RAND() * 100 AS BIGINT) tag -- 為去重后的uuid打上標記司蔬,標記為:0-100之間的整數(shù)沐序。
FROM detail_sdk_session
WHERE partition_date >= '2016-01-01'
AND partition_date <= now
AND uuid IS NOT NULL
GROUP BY uuid -- 通過GROUP BY琉用,保證去重
) t
GROUP BY tag
) s
;
- 第一層SELECT:對uuid進行去重,并為去重后的uuid打上整數(shù)標記
- 第二層SELECT:按照標記進行分組策幼,統(tǒng)計每個分組下uuid的個數(shù)
- 第三層SELECT:對所有分組進行求和
上面這個方法最關(guān)鍵的是為每個uuid進行標記邑时,這樣就可以對其進行分組,分別計數(shù)特姐,最后去和晶丘。如果數(shù)據(jù)量確實很大,也可以增加分組的個數(shù)唐含。例如:CAST(RAND() * 1000 AS BIGINT) tag