窗口函数通过ROW_NUMBER()与日期序号差值构造连续组ID来识别连续打卡段,用LAG()/LEAD()跨行比对班次衔接,RANK()与ROW_NUMBER()依并列需求选用于排班排序,MySQL 5.7需用变量或应用层替代。

用窗口函数识别连续打卡段
连续打卡的本质是:日期连续且状态为“打卡”。关键不是算总天数,而是先切出每一段连续区间。MySQL 8.0+、PostgreSQL、SQL Server 都支持 LAG() 和 ROW_NUMBER(),这是最稳定的做法。
核心思路是:对打卡记录按日期排序,用 ROW_NUMBER() 生成序号,再用日期本身转成“第几天”(比如以最小打卡日为基准),两者相减——同一连续段内,这个差值恒定。
SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp FROM login_log WHERE status = 'checked';
注意:DATE_SUB(... INTERVAL ... DAY) 在 PostgreSQL 要换成 login_date - ROW_NUMBER() OVER (...)::INTEGER;SQLite 不支持直接减整数,得用 julianday() 换算。
按用户+连续组聚合出每段打卡长度
上一步的 grp 是连续段的唯一标识。现在只需按 user_id 和 grp 分组,统计天数、起止日即可:
SELECT
user_id,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS streak_days
FROM (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp
FROM login_log
WHERE status = 'checked'
) t
GROUP BY user_id, grp;
常见错误:漏掉 WHERE status = 'checked',把缺卡日也混入计算;或没 PARTITION BY user_id,导致跨用户误连。
统计分组指标:最长连续、当前连续、连续≥N的人数
- 最长连续:每个用户取
MAX(streak_days)
- 当前连续:取截止今天、且末尾未中断的那段(即
end_date = CURDATE() 的 streak_days)
- 连续≥7天人数:在上层结果中加
HAVING streak_days >= 7 后 COUNT(DISTINCT user_id)
SELECT
COUNT(DISTINCT CASE WHEN streak_days >= 7 THEN user_id END) AS cnt_streak7_plus,
MAX(streak_days) AS max_streak_overall,
AVG(streak_days) AS avg_streak_length
FROM (
-- 上面的连续段子查询
) t;
MAX(streak_days) end_date = CURDATE() 的 streak_days) HAVING streak_days >= 7 后 COUNT(DISTINCT user_id)
性能提示:如果表很大,login_date 和 user_id 必须有联合索引,否则 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) 会全表排序。
MySQL 5.7 或无窗口函数时的替代方案
只能靠自连接或变量模拟。变量法更轻量但不可靠(执行顺序不保证):
SELECT
user_id,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS streak_days
FROM (
SELECT
user_id,
login_date,
@grp := IF(@prev_user = user_id AND DATEDIFF(login_date, @prev_date) = 1, @grp, @grp + 1) AS grp,
@prev_user := user_id,
@prev_date := login_date
FROM login_log, (SELECT @grp := 0, @prev_user := NULL, @prev_date := NULL) r
WHERE status = 'checked'
ORDER BY user_id, login_date
) t
GROUP BY user_id, grp;
这个写法在 MySQL 5.7 中常见,但遇到并发查询或优化器重排时可能出错;上线前必须用小数据集验证结果一致性。
连续段识别这步一旦出错,后续所有指标都偏移。别省略中间验证:先查几个用户的原始打卡日期,再手动比对生成的 grp 是否真把连续日归到一组里。










