① 开篇:这道题为什么值得会
在窗口函数的面试题里,“连续登录 N 天”几乎是绕不开的一类:笔试会让你现场写,面试会让你解释为什么。更关键的是,它并不只考登录:连续签到、连续交易、连续打卡,甚至连续播放、连续下单,都是同一道题换了业务外壳。把日期序列和序号序列对齐,再用一个差值把它们压成组,套路完全一致。所以今天要拿下的不是某一条 SQL,而是一整类“连续”问题。
② 原理:虚拟起点
先把问题说清楚:SQL 怎么知道几条记录“连着”?答案是给连续日期找一个共同标记。把每个用户的登录日期升序编号,连续日期的日期值和序号会同步加一:01-01 配 1,01-02 配 2,01-03 配 3。两者逐行相减,加一和加一互相抵消,差值恒定。这个差值就是“虚拟起点”,它不一定是真实登录日,却像指纹一样标记同一段连续登录。
以 1001 为例:
| user_id | login_date | rn | DATE_SUB(login_date, INTERVAL rn DAY) |
|---|---|---|---|
| 1001 | 2026-01-01 | 1 | 2025-12-31 |
| 1001 | 2026-01-02 | 2 | 2025-12-31 |
| 1001 | 2026-01-03 | 3 | 2025-12-31 |
| 1001 | 2026-01-05 | 4 | 2026-01-01 |
前三天都减出 2025-12-31;01-05 因为中间断了一天,序号只到 4,减出来变成 2026-01-01。差值一变,就说明进入了新的一段。这就是 DATE_SUB 能分组的根本原因,也是面试官问“为什么能这样分组”时最想听到的解释。
③ 模板:完整代码
完整模板只有三层:先用 DISTINCT 去掉重复登录,再在 t2 里算出序号 rn 和差值 grp,最后按 user_id、grp 分组计数。上面的写法把编号和日期减编号放在同一个 CTE 里,结构很紧凑;分组后得到的cnt,就是每一段连续登录的天数。
WITH t1 AS ( SELECT DISTINCT user_id,login_date FROM t_login ), t2 AS( SELECT user_id,login_date,ROW_NUMBER() over(partition by user_id order by login_date) as rn, DATE_SUB(login_date,INTERVAL ROW_NUMBER() over(partition by user_id order by login_date) day) as grp FROM t1 ) select user_id,grp,count(*) as cnt FROM t2 GROUP BY user_id,grp测试数据跑出来的结果:
| user_id | grp | cnt |
|---|---|---|
| 1001 | 2025-12-31 | 3 |
| 1001 | 2026-01-01 | 1 |
④ 易错点:三个坑
第一个坑是不 DISTINCT。同一用户同一天登录两次,ROW_NUMBER 会把同一天编成两个序号,日期减序号的结果随之改变,一段连续登录被硬生生拆开。若 login_date 是 DATETIME,还要先用 DATE() 归一化到天。
第二个坑是漏写 PARTITION BY user_id。多个用户的序号会首尾相接,A 的最后一天和 B 的第一天可能被拼成一段,差值相等只是巧合,不是业务上的连续。
第三个坑是把 grp 当成真实日期。它只是“假设这段从哪天开始、一天不落”算出的虚拟起点,可能根本没人登录过,作用仅是分组标识。面试时说“按虚拟起点分组”比说“按日期分组”准确得多。
⑤ 总结
面试口径可以记成一句:“连续日期减连续序号,差值被抵消成同一个值,按这个值分组就是连续区间。”签到、交易、打卡、连续下单,换掉表名和日期字段即可复用。下一篇如果有需要,可以聊索引失效的六个常见场景。