背景 数据运营人员常常会需要查找活跃用户名单,而活跃用户很多情况下被定义为连续在线或发单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): 最后介绍一个最为直观,也是计算成本最小的方法。假设我们需要求连续登陆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转载https://blog.csdn.net/Adrian_Wang/article/details/89791948