连续登录天数指用户登录日期中无间隔的最长序列天数,需用行号与日期差构造分组键识别断点;核心是date_sub(login_date, interval rn day)生成稳定grp,再按user_id和grp聚合计数取最大值。

连续登录天数的定义必须先明确
“连续登录”不是简单求 MAX(login_date) - MIN(login_date) + 1,而是指用户在日志中出现的、日期无间隔的最长序列。比如用户在 2024-01-01、01-02、01-04 登录,连续段是 [01-01, 01-02](2天)和 [01-04](1天),最大值为 2。核心在于识别“日期断点”,这需要借助行号差(row_number trick)。
用 ROW_NUMBER() 和 DATE_SUB 构造分组标识
关键思路:对每个用户的登录日期排序,再用日期减去行号。同一连续段内,DATE_SUB(login_date, INTERVAL rn DAY) 的结果恒定,可作为分组键。
示例(MySQL 8.0+ 或 PostgreSQL):
SELECT user_id, MAX(cnt) AS max_consecutive_days
FROM (
SELECT user_id,
COUNT(*) AS cnt,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp
FROM user_login_log
GROUP BY user_id, grp
) t
GROUP BY user_id;
注意:login_date 必须是 DATE 类型(非 DATETIME),否则需先 DATE(login_date) 归一化;若数据库不支持窗口函数(如 MySQL 5.7),得用变量模拟 ROW_NUMBER(),但并发下不可靠。
SQLite 和旧版 MySQL 的替代写法
SQLite 没有 ROW_NUMBER(),可用自连接或子查询算“前面有多少个更早且连续的日期”,但性能差;MySQL 5.7 建议用用户变量,但必须确保 ORDER BY 在子查询中生效:
SELECT user_id, MAX(consec) AS max_consecutive_days
FROM (
SELECT user_id,
@rn := IF(@prev = user_id, @rn + 1, 1) AS rn,
@grp := IF(@prev = user_id AND DATEDIFF(login_date, @prev_date) = 1, @grp, @rn) AS grp_id,
@prev := user_id,
@prev_date := login_date,
COUNT(*) OVER (PARTITION BY user_id, @grp) AS consec
FROM (SELECT user_id, DATE(login_date) AS login_date FROM user_login_log ORDER BY user_id, login_date) t,
(SELECT @rn := 0, @prev := '', @prev_date := '1970-01-01', @grp := 0) init
) t
GROUP BY user_id;
这个写法脆弱:变量赋值顺序依赖执行计划,MySQL 8.0+ 应坚决用窗口函数替代。
容易被忽略的边界情况
常见漏判点包括:
-
login_date含时分秒 → 必须先CAST(login_date AS DATE)或DATE(login_date) - 同一天多次登录 → 需先
GROUP BY user_id, DATE(login_date)去重,否则行号错乱 - 跨年连续(如 2023-12-31 → 2024-01-01)→
DATEDIFF或DATE_SUB天数计算天然支持,无需特殊处理 - 空数据或单条记录 → 上述逻辑仍成立,
COUNT(*)会返回 1
真正难的是把“去重 + 排序 + 差值分组”三步串对,中间任意一步出错,连续天数就归零或虚高。











