求连续登录天数,间隔N天也算作连续,以间隔1天为例

问题

需求 : 计算每个用户最大的连续登录天数,可以间隔一天。解释:如果一个用户在当前月份的第 1, 3, 5, 6, 9 天登录游戏,则视为当月连续 6 天登录

CREATE TABLE log (
  id VARCHAR(1),
  time date
);

insert into log values('A','2024-12-20');
insert into log values('A','2024-12-24');
insert into log values('A','2024-12-22');
insert into log values('A','2024-12-25');

insert into log values('A','2024-12-28');
insert into log values('A','2024-12-30');
insert into log values('A','2024-12-31');

insert into log values('B','2024-12-20');
insert into log values('B','2024-12-21');
insert into log values('B','2024-12-23');
insert into log values('B','2024-12-24');

如上表格,正确结果应该输出如下结果。

解释:

  1. A用户有两段连续登录记录,一段是从2024-12-20登录到2024-12-25 ,中间间隔日期只有1天,所以共6天,另一段是2024-12-282024-12-31,中间间隔日期只有15天,共4天。这里请注意区分为什么是两段连续登录而不是一段连续登录,原因:A用户2024-12-282024-12-30是间隔2天。
  2. B用户有一段连续登录记录,是从2024-12-20登录到2024-12-24 ,共5天
  3. A用户取最大连续登录为6天, B用户取最大连续登录是5天
id cont_day
A 6
B 5
方案:

两个方法的核心切入点都是

  1. 求每条记录与前一条记录的差值。
  2. 利用差值找出一个字段,能够区分不同的连续登录片段。

建议先看方案二,再看方案一/方案三,方案二是网络通用方法,方案一是我没学习方案二之前自己想到的。

方法一:

这个方法是我自己想的,

如果间隔大于2,则赋值0,否则赋值1,累计求和diff, 然后用st = time - diff, group by st

虽然能得到正确结果,但是其中一个无意义的字段virtial_start_date很难解释的通,而之所以叫virtual,是因为并不是每段连续记录的开始日期。
这个virtial_start_date更像是:当前连续记录段的第一天时间 - ( 过去N段 (连续连续天数-1) 之总和 ),

而之所以能工作正常,是因为对于每段连续记录,算出的virtual_start_date一定是一样的。

思路:

  1. 使用lag开窗函数 + datediff函数,计算每个记录的日期(time字段)与前一条记录的日期的差值datedff。 注意开窗函数中要 order by 日期。
  2. 如果差值datedff小于等于2,说明间隔小于等于1天,直接保留差值的原本值datedff即可,如果差值大于2天,说明间隔大于1天,已经开始新的连续登录日期了, 我们把差值置为0,这一步处理后的结果命名为新差值,记为diff, diff = 0标志已经开始新连续片段了。
  3. 对新差值diff累加求和sum。注意:开窗函数要加order by, 若无order by则无法实现累加功能,若不懂就去搜搜开窗函数有无order by的影响。

    我们将求和sum记为sum_diff字段。 注意:sum_diff 没有任何实际意义,如果非要硬解释,就是“ 当前时间 - 当前连续登录段的第一天的时间 + ( 过去N段 (连续连续天数-1) 之和 ) “,

    后面这句话是错误的,之前想错了,所以划掉了,可以忽略,sum_diff实际上表达的是“ 与 连续登录区域的第一天的差值” ,这个可以想一想

  4. 使用日期 减去 sum_diff,记为virtual_start_date。某个连续登录段的 virtual_start_date是相同的,注意:virtual_start_date没有任何实际意义,如果非要硬解释,就是:当前连续记录段的第一天时间 - ( 过去N段 (连续连续天数-1) 之和 )。

    后面这句话是错的,之前想错了,所以划掉了,可以忽略,virtual_start_date每段连续登录记录的第一天

  5. group by id, virtual_start_date, 然后求出每个group的最大和最小日期,使用“最大日期 - 最小日期+1” ,就是连续登录连段的连续登录天数,记为cont_day
  6. 再group by id, 求最大的cont_day, 就是答案。
select id, max(cont_day) as max_cont_day
from (
         select id,
                virtual_start_date,
                max(time)                          as maxd,
                min(time)                          as mind,
                datediff(max(time), min(time)) + 1 as cont_day
         from (
                  select id, time, pre_date, diff, sum_diff, date_sub(time, interval sum_diff day) as virtual_start_date
                  from (
                           select id, time, pre_date, diff, sum(diff) over (partition by id order by time) as sum_diff
                           from (
                                    select id,
                                           time,
                                           pre_date,
                                           if(datediff(time, pre_date) > 2, 0, datediff(time, pre_date)) as diff
                                    from (
                                             SELECT id,
                                                    time,
                                                    lag(time, 1, '1970-01-01') over (partition by id order by time) as pre_date
                                             from log
                                         ) t0
                                ) t1
                       ) t2
              ) t3
         group by id, virtual_start_date
     ) t4
group by id
方法二

这个方法是网络上的通用方法,着重记忆这个方法,简单方便

重点:如果间隔大于2,则赋值1,否则赋值0,求和sum_flag, 然后group by sum_flag

思路:

  1. 使用lag开窗函数 + datediff函数,计算每个记录的日期(time字段)与前一条记录的日期的差值datedff。 注意开窗函数中要 order by 日期。
  2. 如果差值datedff小于等于2,说明间隔小于等于1天,则置为0,如果差值大于2天,说明间隔大于1天,已经开始新的连续登录日期了, 我们把差值置为1。 这一步处理后的结果记为flag,flag=1标志已经开始新连续片段了。
  3. 对flag累加求和sum,注意:开窗函数要加order by, 若无order by则无法实现累加功能,若不懂就去搜搜开窗函数有无order by的影响。

    我们将求和sum记为sum_flag。每开始一段新的连续片段时, sum_flag会自增1。

  4. group by id, sum_flag, 然后求出每个group的最大和最小日期,使用“最大日期 - 最小日期+1” ,就是连续登录连段的连续登录天数,记为cont_day
  5. 再group by id, 求最大的cont_day, 就是答案。
select id, max(cont_day) as max_cont_day
from (
         select id, sum_flag, max(time), min(time), datediff(max(time), min(time)) + 1 as cont_day
         from (
                  select id, time, pre_date, flag, sum(flag) over (partition by id order by time) as sum_flag
                  from (
                           select id, time, pre_date, if(datediff(time, pre_date) > 2, 1, 0) as flag
                           from (
                                    SELECT id,
                                           time,
                                           lag(time, 1, '1970-01-01') over (partition by id order by time) as pre_date
                                    from log
                                ) t0
                       ) t1
              ) t2
         group by id, sum_flag
     ) t3
group by id   
其他/方案三:
类似方案一,直接求连续片段开始日期,不依赖聚合函数min()求连续片段开始日期

洁癖点/强迫症:针对方案一,真正求出每段连续登录片段的第一天。

思路:结合方法二中的sum_flag使其分段,然后partiton by user_id, sum_flag,然后分段累加diff。

这个思路唯一的用处仅仅是:求出真正的连续片段的第一天,而不是依赖min() group by 方式求出连续片段的第一天。

select id, max(cont_day) as cont_day
from (
         select id,
                start_date,
                max(time)                          as end_date,
                datediff(max(time), min(time)) + 1 as cont_day
         from (
                  select id,
                         time,
                         pre_date,
                         diff,
                         sum_flag,
                         diff_day,
                         date_sub(time, interval diff_day DAY) as start_date
                  from (
                           select id,
                                  time,
                                  pre_date,
                                  diff,
                                  sum_flag,
                                  sum(diff) over (partition by id, sum_flag order by time) as diff_day
                           from (
                                    select id,
                                           time,
                                           pre_date,
                                           diff,
                                           sum(flag) over (partition by id order by time) as sum_flag
                                    from (
                                             select id,
                                                    time,
                                                    pre_date,
                                                    if(datediff(time, pre_date) > 2, 0, datediff(time, pre_date)) as diff,
                                                    if(datediff(time, pre_date) > 2, 1, 0)                        as flag
                                             from (
                                                      SELECT id,
                                                             time,
                                                             lag(time, 1, '1970-01-01') over (partition by id order by time) as pre_date
                                                      from log
                                                  ) t0
                                             order by id, time
                                         ) t1
                                ) t2
                       ) t3
              ) t4
         group by id, start_date
     ) t5
group by id
总结
  1. 实际上,上述方案,不只适用于求间隔1天,只要修改 datediff(time, pre_date) > N + 1, 就可以实现 ”当间隔N天也算连续登录时,求最大连续登录天数“ 的问题。

    特别的是,若N=0,即不允许间隔登录,也可以用来计算最大连续登录天数的问题。

  2. 方案二是网络上的通用方法,推荐记忆方案二。

    两个重点:(1)如果间隔大于2,则赋值1,否则赋值0。(2)求和sum_flag , 然后group by sum_flag

  3. 如果是求连续片段的开始时间和结束时间,基本就使用max()min()函数实现。

    方案三中,直接求连续片段的开始时间的方法,太过复杂,性价比太低。

参考:
  1. SQL高频面试题:求最大连续登录天数,间隔n天内都可以算做连续登录_sql 最大连续登录次数-CSDN博客
  2. SQL面试题挑战07:间隔连续问题(连续的升级版)_sql不间隔数字连续、数字间间隔几位连续-CSDN博客
  3. 数仓面试——连续登录问题进阶版-腾讯云开发者社区-腾讯云
其他思考(暂未想明白,对本文无用)

当间隔=0,即不允许间隔时,常见的简单的计算最大连续登录的方式如下:

为什么要凭空创造一个row_number呢?

SELECT id, max(cont_day) as cont_day
from
(
  SELECT id, v_start_date, count(id) as cont_day
  from
  (
    SELECT 
      id, time, rn-1,
      date_sub(time, interval (rn-1) DAY) as v_start_date
    from (
             SELECT id,
                    time,
                    row_number() over (partition by id order by time) as rn
             FROM log
         ) t0
  ) t1
  group by id, v_start_date
) t2
group by id

我感觉row_number的出现和方法一很相似。

此方法中,row_number的存在的意义,和方法一中的sum_diff,有相似的感觉?

同理此方法中的v_start_date是否和方法一中的virtual_start_date,有相似的感觉?

感觉sum_diff很像row_number, v_start_date也很像virtual_start_date。

row_number - 1 等于:当前时间 - 当前连续登录段的第一天的时间 + (过去N段连续连续天数之和)

sum_diff等于:当前时间 - 当前连续登录段的第一天的时间 + ( 过去N段 (连续连续天数-1) 之和 )

v_start_date 等于 连续片段第一天 - (过去N段连续连续天数之和)

virtual_start_date 等于 连续片段第一天 - ( 过去N段 (连续连续天数-1) 之和 )

id time rn-1 v_start_date pre_date diff sum_diff virtual_start_date
A 2024-12-20 0 2024-12-20 1970-01-01 0 0 2024-12-20
A 2024-12-22 1 2024-12-21 2024-12-20 0 0 2024-12-22
A 2024-12-24 2 2024-12-22 2024-12-22 0 0 2024-12-24
A 2024-12-25 3 2024-12-22 2024-12-24 1 1 2024-12-24
A 2024-12-28 4 2024-12-24 2024-12-25 0 1 2024-12-27
A 2024-12-30 5 2024-12-25 2024-12-28 0 1 2024-12-29
A 2024-12-31 6 2024-12-25 2024-12-30 1 2 2024-12-29
B 2024-12-20 0 2024-12-20 1970-01-01 0 0 2024-12-20
B 2024-12-21 1 2024-12-20 2024-12-20 1 1 2024-12-20
B 2024-12-23 2 2024-12-21 2024-12-21 0 1 2024-12-22
B 2024-12-24 3 2024-12-21 2024-12-23 1 2 2024-12-22

你可能感兴趣的:(sqlhql)