问题
需求 : 计算每个用户最大的连续登录天数,可以间隔一天。解释:如果一个用户在当前月份的第 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');
如上表格,正确结果应该输出如下结果。
解释:
- A用户有两段连续登录记录,一段是从
2024-12-20
登录到2024-12-25
,中间间隔日期只有1天,所以共6天,另一段是2024-12-28
到2024-12-31
,中间间隔日期只有15天,共4天。这里请注意区分为什么是两段连续登录而不是一段连续登录,原因:A用户2024-12-28
到2024-12-30
是间隔2天。 - B用户有一段连续登录记录,是从
2024-12-20
登录到2024-12-24
,共5天 - A用户取最大连续登录为6天, B用户取最大连续登录是5天
id | cont_day |
---|---|
A | 6 |
B | 5 |
方案:
两个方法的核心切入点都是
- 求每条记录与前一条记录的差值。
- 利用差值找出一个字段,能够区分不同的连续登录片段。
建议先看方案二,再看方案一/方案三,方案二是网络通用方法,方案一是我没学习方案二之前自己想到的。
方法一:
这个方法是我自己想的,
如果间隔大于2,则赋值0,否则赋值1,累计求和diff, 然后用st = time - diff, group by st
虽然能得到正确结果,但是其中一个无意义的字段virtial_start_date很难解释的通,而之所以叫virtual,是因为并不是每段连续记录的开始日期。
这个virtial_start_date更像是:当前连续记录段的第一天时间 - ( 过去N段 (连续连续天数-1) 之总和 ),而之所以能工作正常,是因为对于每段连续记录,算出的virtual_start_date一定是一样的。
思路:
- 使用
lag
开窗函数 + datediff函数,计算每个记录的日期(time字段)与前一条记录的日期的差值datedff。 注意开窗函数中要 order by 日期。 - 如果差值datedff小于等于2,说明间隔小于等于1天,直接保留差值的原本值datedff即可,如果差值大于2天,说明间隔大于1天,已经开始新的连续登录日期了, 我们把差值置为0,这一步处理后的结果命名为新差值,记为diff, diff = 0标志已经开始新连续片段了。
对新差值diff累加求和sum。注意:开窗函数要加order by, 若无order by则无法实现累加功能,若不懂就去搜搜开窗函数有无order by的影响。
我们将求和sum记为sum_diff字段。 注意:sum_diff 没有任何实际意义,如果非要硬解释,就是“ 当前时间 - 当前连续登录段的第一天的时间 + ( 过去N段 (连续连续天数-1) 之和 ) “,
后面这句话是错误的,之前想错了,所以划掉了,可以忽略,
sum_diff实际上表达的是“ 与 连续登录区域的第一天的差值” ,这个可以想一想。使用日期 减去 sum_diff,记为virtual_start_date。某个连续登录段的 virtual_start_date是相同的,注意:virtual_start_date没有任何实际意义,如果非要硬解释,就是:当前连续记录段的第一天时间 - ( 过去N段 (连续连续天数-1) 之和 )。
后面这句话是错的,之前想错了,所以划掉了,可以忽略,
virtual_start_date每段连续登录记录的第一天,- group by id, virtual_start_date, 然后求出每个group的最大和最小日期,使用“最大日期 - 最小日期+1” ,就是连续登录连段的连续登录天数,记为cont_day
- 再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
思路:
- 使用
lag
开窗函数 + datediff函数,计算每个记录的日期(time字段)与前一条记录的日期的差值datedff。 注意开窗函数中要 order by 日期。 - 如果差值datedff小于等于2,说明间隔小于等于1天,则置为0,如果差值大于2天,说明间隔大于1天,已经开始新的连续登录日期了, 我们把差值置为1。 这一步处理后的结果记为flag,flag=1标志已经开始新连续片段了。
对flag累加求和sum,注意:开窗函数要加order by, 若无order by则无法实现累加功能,若不懂就去搜搜开窗函数有无order by的影响。
我们将求和sum记为sum_flag。每开始一段新的连续片段时, sum_flag会自增1。
- group by id, sum_flag, 然后求出每个group的最大和最小日期,使用“最大日期 - 最小日期+1” ,就是连续登录连段的连续登录天数,记为cont_day
- 再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天,只要修改 datediff(time, pre_date) > N + 1, 就可以实现 ”当间隔N天也算连续登录时,求最大连续登录天数“ 的问题。
特别的是,若N=0,即不允许间隔登录,也可以用来计算最大连续登录天数的问题。
方案二是网络上的通用方法,推荐记忆方案二。
两个重点:(1)如果间隔大于2,则赋值1,否则赋值0。(2)求和sum_flag , 然后group by sum_flag
如果是求连续片段的开始时间和结束时间,基本就使用
max()
和min()
函数实现。方案三中,直接求连续片段的开始时间的方法,太过复杂,性价比太低。
参考:
- SQL高频面试题:求最大连续登录天数,间隔n天内都可以算做连续登录_sql 最大连续登录次数-CSDN博客
- SQL面试题挑战07:间隔连续问题(连续的升级版)_sql不间隔数字连续、数字间间隔几位连续-CSDN博客
- 数仓面试——连续登录问题进阶版-腾讯云开发者社区-腾讯云
其他思考(暂未想明白,对本文无用)
当间隔=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 |