用窗口函数识别连续日期段的核心是通过to_days(log_date) - row_number() over (partition by user_id order by log_date)构造恒定分组标识,再按用户和分组聚合求最长连续天数。

用窗口函数识别连续日期段
核心是把同一用户连续的打卡日期划成一组。关键在于:相邻日期的差值固定,但日期本身在变,所以要用 date_sub() 或 datediff() 把日期“归一化”成一个分组标识。
常见错误是直接对 log_date 做 row_number() 然后相减——这只能用于整数序列,日期必须先转为可计算的数值差。MySQL 8.0+、PostgreSQL、SQL Server 都支持 datediff(day, '1970-01-01', log_date) 这类写法;MySQL 5.7 可用 to_days(log_date) 替代。
实操建议:
- 先按
user_id和log_date排序,确保时间顺序 - 构造分组键:
to_days(log_date) - row_number() over (partition by user_id order by log_date) - 这个差值在连续日期下恒定,中断后重置,就是天然的“连续段 ID”
按用户和连续段聚合求最大天数
有了连续段 ID,就可对每个 user_id + 段 ID 统计天数,再取每个用户的最大值。注意:不是统计总打卡天数,而是最长连续段长度。
容易踩的坑:
- 漏掉
partition by user_id,导致跨用户误分组 - 在子查询里用了
group by user_id, segment_id却没加max(segment_length)的外层聚合 - 日期字段含时分秒(如
datetime类型),未用date(log_date)截断,造成同一天多次打卡被拆成多行
简短示例(MySQL):
SELECT user_id, MAX(cnt) AS max_consecutive_days
FROM (
SELECT user_id,
to_days(log_date) - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY log_date) AS segment_id,
COUNT(*) AS cnt
FROM打卡表
WHERE log_date IS NOT NULL
GROUP BY user_id, segment_id
) t
GROUP BY user_id;
处理数据缺失或重复打卡
真实场景中,用户可能一天打多次卡,或某天漏打但后续补上——这两类都会干扰连续性判断。
必须前置清洗:
- 用
SELECT DISTINCT user_id, DATE(log_date) AS log_date去重,避免同日多次打卡膨胀段长度 - 若业务允许“补卡”,需确认补录日期是否参与连续计算;通常应以实际打卡时间为准,而非补录时间
- 若原始数据有空值或非法日期(如
'0000-00-00'),to_days()会返回NULL,导致整个 segment_id 为NULL,该记录会被排除——这点要提前验证
性能与兼容性提醒
窗口函数在 MySQL 8.0、PostgreSQL 8.4+、SQL Server 2005+ 支持良好,但 SQLite 3.25+ 才支持 ROW_NUMBER(),旧版本需改用自连接模拟,性能极差。
当打卡表超千万行时:
- 务必在
(user_id, log_date)上建联合索引 - 避免在
WHERE中对log_date用函数(如date(log_date) = ...),否则索引失效 - 如果只查最近 30 天,先用日期范围过滤再开窗,别让窗口扫全表
连续天数逻辑看着简单,真正跑通要盯住三处:日期归一化是否一致、去重是否彻底、索引是否生效。漏掉任意一个,结果就可能偏高或偏低。










