识别连续日期或序号分组的核心思路是:排序后原始序号(ROW_NUMBER)与业务字段(如日期、ID)的差值恒定,该差值相同即属同一连续区间;须用ROW_NUMBER而非RANK,日期需归一化,且必须PARTITION BY业务键。

怎么识别连续日期或序号的分组
核心思路是:连续记录在排序后,其原始序号(如 ROW_NUMBER())和业务字段(如日期、ID)之间的差值恒定。只要这个差值相同,就属于同一段连续区间。
常见错误是直接用 GROUP BY date_col 或 GROUP BY id —— 这只能聚合相同值,无法捕获“连续”这个动态关系。
- 业务字段必须可排序(如
DATE、INT),且理想情况下无重复值;若存在重复,需先去重或定义优先级(比如取MIN(created_at)) - 用
ROW_NUMBER() OVER (ORDER BY date_col)生成严格递增序号,不是RANK()或DENSE_RANK(),后者遇并列会跳号,破坏差值稳定性 - 日期类字段建议转为天数差(如
date_col - '2000-01-01'::DATE),避免直接减 timestamp 导致精度干扰
PostgreSQL / MySQL 8.0+ 实现连续段计数
以统计每个用户连续登录天数为例,目标是:对每个 user_id,找出最长连续登录天数。
关键在于两层嵌套:第一层算差值分组,第二层按该分组聚合长度。
SELECT user_id, COUNT(*) AS consecutive_days
FROM (
SELECT user_id, login_date,
login_date - INTERVAL '1 day' * ROW_NUMBER()
OVER (PARTITION BY user_id ORDER BY login_date) AS grp
FROM user_logins
) t
GROUP BY user_id, grp;
-
INTERVAL '1 day' * ROW_NUMBER()是 PostgreSQL 写法;MySQL 需用DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (...) DAY) - 如果字段是
INT类型 ID(如订单号连续),直接写id - ROW_NUMBER() OVER (...)即可 - 注意
PARTITION BY user_id必须加上,否则不同用户的差值会混在一起
遇到 NULL 或间隔太大会怎样
差值法对数据质量敏感:一旦中间有缺失、NULL 或异常大间隔,就会被切分成多个组——这其实是正确行为,不是 bug。
但容易误判的情况包括:
-
login_date是TIMESTAMP且含时分秒,导致同一天多条记录被拆开 → 应先用DATE(login_date)归一化 - 表中存在未来日期或乱序数据 → 差值计算前必须加
ORDER BY,且建议 WHERE 过滤掉明显异常值(如login_date > NOW() + INTERVAL '7 days') - 连续判断逻辑实际要求“每天至少一条”,但数据里某天只有凌晨一条、晚上一条 → 若业务允许,可提前用
DISTINCT ON (user_id, DATE(login_date))去重
性能和兼容性要注意什么
窗口函数本身不慢,但差值法依赖全排序,数据量大时 ORDER BY 成为瓶颈。
- 务必在
PARTITION BY和ORDER BY字段上建联合索引,例如(user_id, login_date) - SQL Server 2012+、Oracle 11gR2+、BigQuery 都支持,但 SQLite 直到 3.25 才支持窗口函数,旧版需改用自连接模拟(极慢,不推荐)
- 如果只要“是否存在连续3天”,可用
LEAD()/LAG()查相邻行,比差值法更轻量,也更容易加索引优化
差值法看着绕,但它是目前最通用、最可控的解法;真正难的不是写出来,而是想清楚“连续”在你业务里到底指什么——是自然日?工作日?还是系统内单调递增的某个序列?这个定义错了,后面全白搭。










