连续登录问题
问题
连续登录问题,是实际分析需求和面试中常见的问题。
其实,连续登录问题常规来说有两种场景,其难度完全不一样。
第一种是指定连续登录天数,求满足的用户
第二种则是求用户连续登录了多少天。
本文只是简单探讨。
语法
本文使用的spark sql的语法,如有差异,自行替换。
表结构&数据
明细就直接忽略了,直接以用户登录日期粒度的表结构为主,实际需求中可以自行聚合到这个粒度。
表结构&数据
user_id | login_date |
---|---|
1 | ‘2023-01-01’ |
1 | ‘2023-01-02’ |
1 | ‘2023-01-03’ |
1 | ‘2023-01-04’ |
1 | ‘2023-01-11’ |
1 | ‘2023-01-12’ |
1 | ‘2023-01-15’ |
2 | ‘2023-01-15’ |
3 | ‘2023-01-01’ |
3 | ‘2023-01-02’ |
3 | ‘2023-01-03’ |
1 | with user_log as ( |
场景
场景1 给定具体日期,且指定连续登录天数
例如: 求在2023-01-03号那天连续登录3天的用户。
这种场景是最简单的,直接聚合+having
1 | select user_id |
结果:
user_id | cnt |
---|---|
1 | 3 |
1 | 3 |
场景2,不给定具体日期,但指定连续登录天数
例如:求用户在哪几天连续登录2天。
这部分需要用到窗口函数了,同时由于日期间隔为1,可以使用range特性
1 | select * |
结果如下
user_id | login_date | continue_cnt |
---|---|---|
1 | 2023-01-01 | 1 |
1 | 2023-01-02 | 2 |
1 | 2023-01-03 | 2 |
1 | 2023-01-04 | 2 |
1 | 2023-01-11 | 1 |
1 | 2023-01-12 | 2 |
1 | 2023-01-15 | 1 |
2 | 2023-01-15 | 1 |
3 | 2023-01-01 | 1 |
3 | 2023-01-02 | 2 |
3 | 2023-01-03 | 2 |
结果没有过滤,是为了方便理解,大家自行加上continue_cnt=2即可。
场景3,既不给定具体业务日期,也不指定连续登录天数。
这种场景是最难的,最耗性能的,但也是实用性最高的。
row_number 解法
说实话,这个解法本人没有想出来,是网上搜到的,使用row_number进行排序,利用日期差特性,如果是连续的,那么实际的差值日期是一样的特性。
1 | ,login_date_grp as ( |
结果如下
user_id | login_date | continue_login_days_cnt | login_date_rn | continue_date_grp |
---|---|---|---|---|
1 | 2023-01-01 | 1 | 1 | 2022-12-31 |
1 | 2023-01-02 | 2 | 2 | 2022-12-31 |
1 | 2023-01-03 | 3 | 3 | 2022-12-31 |
1 | 2023-01-04 | 4 | 4 | 2022-12-31 |
1 | 2023-01-11 | 1 | 5 | 2023-01-06 |
1 | 2023-01-12 | 2 | 6 | 2023-01-06 |
1 | 2023-01-15 | 1 | 7 | 2023-01-08 |
2 | 2023-01-15 | 1 | 1 | 2023-01-14 |
3 | 2023-01-01 | 1 | 1 | 2022-12-31 |
3 | 2023-01-02 | 2 | 2 | 2022-12-31 |
3 | 2023-01-03 | 3 | 3 | 2022-12-31 |
可以看到,结果是符合我们预期的,这时候只要外面再套上一层过滤条件即可。
看的稍微有点蒙?这里稍微解释一下,这里巧妙的利用了等差数组的特性。
比如有如下一个用户连续5天登录的数据,同时我们对同一用户按照日期进行row num排序,再求每一行和’2023-01-01’这一天的差值日期,然后再对这两列求差值,会看到如下结果
用户id | 登录日期 | 日期排序 | 和’2022-12-31’的间隔天数 | 日期排序和’2023-01-01’的间隔天数的差值 |
---|---|---|---|---|
1 | 2023-01-01 | 1 | 1 | 0 |
1 | 2023-01-02 | 2 | 2 | 0 |
1 | 2023-01-03 | 3 | 3 | 0 |
1 | 2023-01-04 | 4 | 4 | 0 |
1 | 2023-01-05 | 5 | 5 | 0 |
我们可以观察到,如果日期是连续的,那么本身和’2022-12-31’的间隔天数这一列就是+1的规则,而row num也是自然+1的,差值永远是1
此时,如果删除2023-01-03这一天的登录记录,那么结果就会变为
用户id | 登录日期 | 日期排序 | 和’2022-12-31’的间隔天数 | 日期排序和’2022-12-31’的间隔天数的差值 |
---|---|---|---|---|
1 | 2023-01-01 | 1 | 1 | 0 |
1 | 2023-01-02 | 2 | 2 | 0 |
1 | 2023-01-04 | 3 | 4 | 1 |
1 | 2023-01-05 | 4 | 5 | 1 |
可以看到,随着2023-01-03这条记录被删除,在2023-01-04这一天,间隔从0变为了1,而2023-01-05这天还是连续登录的,间隔仍然是1. |
此时就能发现规律了,如果是连续登录的,那么间隔差值就不会变,这里其实可以用数学公式证明,这里就补展开了。
所以,为什么代码里用的是date_sub作为分组?其实我们要求的是差值,而日期本身就是连续的数字,date_sub(日期,rn) 是不是就是可以看成差值。 还节约了计算资源。
这题的解法,本人认为还是有很多精巧设计在里面的,一般确实很难想到。
使用日期维表获取上一次未登陆日期
这个是本人想到的,可以利用日期维表,补全没有登录的天数,然后利用窗口函数求出上一次未登陆日期,再利用datediff求出差值即连续登录天数。
1 | dim_date as ( |
结果如下
user_id | login_date | continue_days | last_unlogin_date |
---|---|---|---|
1 | 2023-01-01 | 1 | null |
1 | 2023-01-02 | 2 | null |
1 | 2023-01-03 | 3 | null |
1 | 2023-01-04 | 4 | null |
1 | 2023-01-11 | 1 | 2023-01-10 |
1 | 2023-01-12 | 2 | 2023-01-10 |
1 | 2023-01-15 | 1 | 2023-01-14 |
2 | 2023-01-15 | 1 | 2023-01-14 |
3 | 2023-01-01 | 1 | null |
3 | 2023-01-02 | 2 | null |
3 | 2023-01-03 | 3 | null |
可以看到结果符合预期。
总结
一直以为自己sql水平还是不错的,原来以前一直没怎么处理过连续登录问题,面试被问了还是懵逼了一下。该总结的还得总结一下。
总的来说,连续登录还是比较难的,尤其是考虑性能的情况下,使用row_number利用等差数据确实是一个很精妙的解法。