原文地址:
1.Hive SQL複雜場景實現(1) —— 連續發單天數
https://blog.csdn.net/Adrian_Wang/article/details/89791948
至今在數據分析崗摸爬滾打已有一年,尚且不敢說自己挖掘洞見的本事提升多少。但實打實的與SQL打了一年的交道,接觸過各種各樣的業務場景,完成過各種千奇百怪的需求,自我感覺在sql編程上也頗有體會。
相信接觸過SQL的人都明白知道其非常容易上手,作爲一個結構化查詢語言,其在數據提取上給人們提供了非常大的便捷。然而在考慮到開發成本和計算的複雜度的情況下,並非所有的提數需求都適合用sql來實現,也並非sql能夠在各種業務場景下提供數據提取的最優解。有些時候hive streaming、spark、甚至簡單的python腳本都能把sql難以實現的邏輯變得得心應手。即便如此,探究sql在一些複雜場景下的實現還是對鍛鍊邏輯思維很有幫助。
最近我會專門開闢一個專欄來分享這一年來我所遇到的一些比較複雜的業務場景,併力求通過純sql的方法實現,給出我能想到的所有解法與大家分享。這也算是見證和記錄自己一年來sql編程的心路歷程了吧。
畢竟我的目標是data scientist呀
————————————————
背景
數據運營人員常常會需要查找活躍用戶名單,而活躍用戶很多情況下被定義爲連續在線或發單n天及以上的用戶。一方面我們可以根據n的值直接進行篩選;更具一般性地,就要求我們去求取每個用戶某段時間內的最大連續在線或者發單天數了。
SQL求連續在線天數是一個非常經典的問題,該問題在不考慮計算成本下有非常多的解法。該問題也是我在面試實習生時最喜歡深入問的一個問題,在引導一個候選人去完成這個問題的過程中可以看出其對sql的理解深度以及其思維是否靈敏。
該問題的最大難點在於如何判斷日期與日期間是否連續,那這就要涉及到處理行與行之間的關係了。說到這對SQL比較熟悉的同學應該就會反應出使用join或者窗口函數來處理了。
數據:
假設我們有19年一月份每日用戶發單數據存儲於訂單表order_base:
user_id | order_id | create_time |
---|---|---|
234520012 | 1231512416323 | 2019-01-02 12:21:11 |
123149908 | 2412298719221 | 2019-01-04 01:11:34 |
… | … | … |
相關解法
解法1(通過與特定日期的日期差判定連續):
本方法比較tricky。連續的時間以爲着這些時間點與某一個特定時間點的時間差也是連續的,從下表可以直觀理解這一點:
日期 | 特定日期 | 日期差d |
2019-01-01 | 2019-01-01 | 0 |
2019-01-02 | 2019-01-01 | 1 |
2019-01-04 | 2019-01-01 | 3 |
2019-01-05 | 2019-01-01 | 4 |
2019-01-06 | 2019-01-01 | 5 |
那麼我們對該日期差d進行個排序,如果連續的話,d與序號的差值應該是相同的,如下表:
日期 | 特定日期 | 日期差d | 序號r | 日期差d與序號r的差值 |
2019-01-01 | 2019-01-01 | 0 | 0 | 0 |
2019-01-02 | 2019-01-01 | 1 | 1 | 0 |
2019-01-04 | 2019-01-01 | 3 | 2 | 1 |
2019-01-05 | 2019-01-01 | 4 | 3 | 1 |
2019-01-06 | 2019-01-01 | 5 | 4 | 1 |
這樣答案就顯而易見了,只需要對上面這個子查詢的最後一列進行分組統計行數,變得到了每次連續的天數,再取連續天數的最大值,便是我們想要的答案。
select
user_id,
max(date_cnt) as max_continuation_date_cnt
from
(
select
user_id,
d-d_ranking as d_group, -- 連續日期的組標記
count(1) as date_cnt
from
(
select
user_id,
d, --與標記日期的日期差
row_number() over(partition by user_id order by d) as d_ranking --與標記日期的日期差的排序
from
(
select
user_id,
datediff(create_date,'2019-01-01') as d --與標記日期的日期差
from
(
select
user_id,
to_date(create_time) as create_date
from
order_base
group by
user_id,
date(create_time)
)a -- 在這一層獲取用戶的發單日期並去重
)b --這一層獲取與標記日期的日期差
)c --獲取連續日期的排序
group by
user_id,
d-d_ranking
)d -- 獲取每一個連續日期組的連續天數
group by
user_id
解法2(left join進行笛卡爾積):
假設我們不需要知道用戶最大的連續天數,只需要知道某個用戶是否出現連續n天(假設n爲3)登錄的行爲。那這裏首先給出一種完全不考慮計算複雜度的解法,使用純join關聯去實現該問題。
整體思路是去獲得同一個用戶的發單日期對,看每一個發單日期的n天內是否有n個發單日期。
select
user_id
from
(
select
user_id,
to_date(create_time) as create_date
from
order_base
group by
user_id,
date(create_time)
)a -- 在這一層獲取用戶的發單日期並去重
left join
(
select
user_id,
to_date(create_time) as create_date
from
order_base
group by
user_id,
to_date(create_time)
)b -- 與a完全相同的邏輯,爲了得到日期與日期間的關聯
on
a.user_id = b.user_id --僅使用user_id進行關聯,獲取同一個用戶發單日期間的笛卡爾積
where
a.create_date <= b.create_date
and date_add(a.create_date,3) > b.create_date --以a的日期爲基準,保留從a.create_date開始的3天內發單日期
group by
user_id,
a.create_date
having
count(1) = 3 --如果從a.create_date開始的3天內都有發單,則應該有3條記錄
該方法容易理解,但其最大的弊端在於關聯時造成的笛卡爾積大大增加了計算的複雜度。在較小的數據集上可以考慮該方法,但實際生產環境下意義並不大。
解法3 (lead或lag):
這裏 使用到了 HIVE 窗口函數 LAG LEAD, 不熟悉的同學請參考 我的文章 :
1. HIVE_HIVE函數_窗口函數_LAG()/LEAD() 詳解
https://blog.csdn.net/u010003835/article/details/106739353
最後介紹一個最爲直觀,也是計算成本最小的方法。假設我們需要求連續登陸n天(假設n爲7)及以上的用戶,那麼對於一個存在該行爲的用戶,他去重和排序後的發單日期信息中,必存在某一天,往前回溯(往後推)6條記錄的日期,等於該日期減6(加6)。這麼說可能不太好理解,但相信你看了以下代碼便能很快明白我在說什麼:
select
user_id
from
(
select
user_id,
create_date,
lag(create_date,6,null) over(partition by user_id order by create_date) as last_6_row -- 按時間排序後6行之前的那一條記錄
(
select
user_id,
to_date(create_time) as create_date
from
order_base
group by
user_id,
date(create_time)
)a -- 在這一層獲取用戶的發單日期並去重
)b --獲取6行之前的那一條記錄
where
datediff(create_date,last_6_row) = 6
group by
user_id
總結:
如我在介紹問題背景的時候所說,處理日期間的連續性就需要將行與行之間進行關聯,而sql提供的解決方案是join和窗口函數。恰恰sql的優勢便在於刻畫這種行數據間的關係,該問題場景能夠幫助我們更深入地理解SQL的這一特性。