關於留存率的SQL語句,之前看到猴子分析那里給過一個思路,是用timestampdiff函數來求,而且有一個模板,可以統一求次日留存率、三日留存率、七日留存率之類的,但是在牛客網刷題也遇到一些留存率分析的題目,發現試圖套模板出了問題,因此這里梳理總結一下思路。
先看數據如下:表:login,字段分別有id、user_id、client_id、date,分別表示序號、用戶id、設備號、登錄日期

然后要求輸出格式如下:

一、猴子分析的求留存率思路:
1.求出每天的活躍用戶數:
select date,count(distinct user_id) as 活躍用戶數 from login group by date
顯示結果如下:

2.求出次日留存數:
利用自聯結【一個表如果涉及到時間間隔,就需要用到自聯結,也就是將兩個相同的表進行聯結】,將兩個表連起來,使用timestampdiff(unit,begin,end)求出時間間隔,然后用case when 計算出時間間隔=1的用戶數量,即次日留存數。
注意:這里猴子分析的表述有點歧義,真正的自聯結,應該是給同一張表起別名,比如loign as a,login as b,然后再在where里面加條件,但其實猴子分析這里提供的連接是用的外連接里的左連接 left join,將同一張表起別名連接起來。
自連結:
select a.user_id,a.date as d1,b.date as d2 from login as a,login as b where a.user_id = b.user_id

左連接:
select a.user_id,a.date as d1,b.date as d2 from login as a left join login as b on a.user_id = b.user_id

可以看出來查出來結果除了順序有點不一樣以外,其余內容本質上是一樣的。
然后使用timestampdiff(unit,begin,end)求出時間間隔,然后用case when 計算出時間間隔=1的用戶數量,即次日留存數。
select d1,count(distinct case when day_diff = 1 then user_id else null end) as 次日留存數
from(select *,timestampdiff(day,d1,d2) as day_diff from( select a.user_id,a.date as d1,b.date as d2 from login as a left join login as b on a.user_id = b.user_id) as c) as d group by d1

3.求出次日留存率
留存率 = 新增用戶中登錄用戶數/新增用戶數,所以次日留存率 = 次日留存用戶數/當日用戶活躍數
這是猴子分析中給出的公式,注意是有問題的,具體問題下面會說!
當日活躍用戶數是count(disctinct 用戶id),在上面分析次日留存數中,用次日留存用戶數/當日用戶活躍數就是次日留存率
select d1,count(distinct case when day_diff = 1 then user_id else null end) as 次日留存數, count(distinct case when day_diff = 1 then user_id else null end)/count(distinct user_id) as 次日留存率 from(select *,timestampdiff(day,d1,d2) as day_diff from( select a.user_id,a.date as d1,b.date as d2 from login as a,login as b where a.user_id = b.user_id) as c) as d group by d1

但這個結果跟牛客網給出的答案是不符合的!因為2020-10-14號這天的次日留存率標准答案應該是1!這個問題就在於猴子分析給出的次日留存率公式是有問題的!次日留存率應該是次日留存用戶數/當日新增用戶數,重點就是分母應該是新增用戶數,而不是猴子分析里給出的活躍用戶數!這種算法會把老用戶也算進去,如果仔細觀察原始數據表login就會知道,2020-10-14號登錄的用戶有兩個,分別是user_id為3和user_id為4的,但2020-10-15號登錄的用戶只有user_id為4的用戶了,所以按照猴子分析的算法2020-10-14號這天的次日留存率會是0.5。但實際上user_id為3的用戶早在2020-10-12就登錄了,所以他根本不能被算在2020-10-14的新增用戶里,他只是一個老用戶!!!
所以想真正的計算出每個日期新用戶的次日留存率必須要計算出兩張表,一張是每天的新用戶表,一張是每天的留存用戶表!下面給出牛客網的大神的解題思路:
二、牛客大神的統計新用戶留存率思路:
以集合的思想來理解,以date對整體進行划分,划分為之后每個集合里需要包括每個date的新用戶數,每個date的留存用戶數,這兩部分相除組成留存率
select date,xxx from group by date
基於此進一步完善我們想要的框架:
select c.date,round( count(d.user_id)/count(),3)as p from(每天的新用戶表) as c left join(每天的留存用戶表) as d on c.user_id = d.user_id group by c.date
接下來再分別寫一個新用戶表和留存用戶表,完了塞進上述結構里去:
新用戶表,別名c
select a.date,b.user_id from (select distinct l1.date from login l1) as a left join (select l2.user_id,min(l2.date) as f_date from login l2 group by l2.user_id) as b on a.date = b.f_date
表a:【表a存在的目的是為了把所有日期展示出來,否則只會有出現新用戶登錄的12和14號,13號和15號就被遺漏了】

表b:

最終查出來的每天的新用戶表(別名c)如下:

留存用戶表,別名d【留存用戶表因為涉及到時間間隔=1的問題,所以用之前猴子分析中提到的兩表連接最簡單了(自連結左連接都行)】
select distinct l3.user_id from login l3,login l4 where l3.user_id =l4.user_id and date_add(l3.date,interval 1 day)=l4.date #這里換成 and timestampdiff(day,l3.date,l4.date)=1 一樣的效果

最后將新用戶表c和留存用戶表d連接起來,以新用戶表c為主表,左連接留存用戶表d:
select * from(select a.date,b.user_id from (select distinct l1.date from login as l1) as a left join (select user_id,min(date) as m_date from login as l2 group by user_id) as b on a.date = b.m_date) as c left join(select distinct l3.user_id from login l3,login l4 where l3.user_id =l4.user_id and date_add(l3.date,interval 1 day)=l4.date) as d on c.user_id = d.user_id

然后塞進一開始的框架里,組成完整的SQL查詢語句:
select date,round(count(distinct d.user_id)/count(distinct c.user_id),3) as p from(select a.date,b.user_id from (select distinct l1.date from login as l1) as a left join (select user_id,min(date) as m_date from login as l2 group by user_id) as b on a.date = b.m_date) as c left join(select distinct l3.user_id from login l3,login l4 where l3.user_id =l4.user_id and date_add(l3.date,interval 1 day)=l4.date) as d on c.user_id = d.user_id group by date

如果要求結果一定要顯示為0而不是NULL的話,可以再加一個ifnull()函數,把NULL替換成0

select date,ifnull(round(count(distinct d.user_id)/count(distinct c.user_id),3),0)as p

