mysql 8.0+用datediff与窗口函数识别连续登录段:先按用户和日期去重排序,再用date_sub(login_date, interval rn day)生成连续组标识,最后按user_id和group_id分组统计起止日期及天数。

用 DATEDIFF + 窗口函数识别连续登录段
MySQL 8.0+ 才支持窗口函数,这是解题的关键前提。低版本只能靠自连接或变量模拟,但极易出错且性能差。核心思路是:对每个用户的登录日期排序,再用 DATE_SUB(login_date, INTERVAL rn DAY) 计算“连续组标识”,相同标识即为同一连续段。
常见错误是直接用 LAG() 比较前一天是否存在——这只能判断是否隔天登录,无法识别“连续3天及以上”的完整区间。必须先分组再统计长度。
实操建议:
- 确保
login_date是DATE类型(不是DATETIME),否则需先DATE(login_time)转换 - 先去重:同一用户同一天多次登录只计1次,加
DISTINCT user_id, DATE(login_time)或用GROUP BY预处理 - 排序必须用
ORDER BY login_date,否则ROW_NUMBER()乱序会导致分组断裂
构造连续组标识(group_id)的写法细节
连续登录的本质是:日期序列与行号序列的差值恒定。比如登录日为 ['2024-01-01','2024-01-02','2024-01-03'],对应行号 [1,2,3],则 DATE_SUB(login_date, INTERVAL rn DAY) 全部等于 '2023-12-31' —— 这就是 group_id。
注意点:
- 不能写成
login_date - rn(MySQL 会尝试数值减法,结果不可控) - 必须用
DATE_SUB(login_date, INTERVAL rn DAY)或等价的login_date - INTERVAL rn DAY - 如果日期字段含时分秒,务必先
DATE(login_time),否则同一天不同时间会被拆成多行
示例片段:
SELECT user_id, login_date,
DATE_SUB(DATE(login_time), INTERVAL rn DAY) AS group_id
FROM (
SELECT user_id, login_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY DATE(login_time)) AS rn
FROM user_login
GROUP BY user_id, DATE(login_time)
) t;
筛选连续天数 ≥ 3 的用户及起止日期
拿到 group_id 后,按 user_id 和 group_id 分组,用 COUNT(*) 算连续天数,再取 MIN(login_date) 和 MAX(login_date) 即可得区间。
容易被忽略的边界情况:
- 用户在多个时间段都满足连续≥3天(如1月1–3日、1月10–12日),结果要返回全部段,不能只取最早一段
-
HAVING COUNT(*) >= 3必须写在最内层分组后,不能放在外层过滤,否则会漏掉中间段 - 若只要用户ID不要具体日期,可在外层
SELECT DISTINCT user_id,但会丢失连续段信息
最终查询骨架:
SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS days
FROM (
SELECT user_id, DATE(login_time) AS login_date,
DATE_SUB(DATE(login_time), INTERVAL ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY DATE(login_time)
) DAY) AS group_id
FROM user_login
GROUP BY user_id, DATE(login_time)
) g
GROUP BY user_id, group_id
HAVING COUNT(*) >= 3;
MySQL 5.7 及更低版本的替代方案
没有窗口函数时,@row_number 变量方案看似可行,但在多用户并发查询或优化器重排时行为不稳定,官方已明确不推荐用于生产。更可靠的做法是用自连接计算前N天是否存在记录:
例如查“是否存在某用户在 login_date 前2天也登录过”:
- 对每条记录,LEFT JOIN 自身两次,分别匹配
t2.login_date = DATE_SUB(t1.login_date, INTERVAL 1 DAY)和t3.login_date = DATE_SUB(t1.login_date, INTERVAL 2 DAY) - WHERE t2.user_id IS NOT NULL AND t3.user_id IS NOT NULL,即可找出连续第3天的“终点”
- 但此法只能定位连续段末尾,无法直接得出完整区间;且 N 增大时 JOIN 数量指数增长,N=7 就很难扛住
结论:除非无法升级 MySQL,否则别碰变量或深层自连接。升级到 8.0 是最省心的解法。
真正难的不是写出 SQL,而是确认业务定义里的“连续”是否包含节假日、是否允许同天多次登录只计1次——这些逻辑必须和产品对齐,否则代码再准也没用。











