组号偏移量法最稳定,因它利用“连续自然数序列差值恒定”原理,通过dt - (rn - 1) * interval '1 day'生成唯一组标识,避免lag()漏首日、自连接跨月出错等问题,且不依赖递归或高级窗口特性。

直接说结论:用“组号偏移量”(即基于 ROW_NUMBER() 差值分组)是统计连续达标天数最稳定、不依赖递归和窗口函数高级特性的通用解法,适用于 MySQL 8.0+、PostgreSQL、SQL Server、Oracle 等主流数据库。
为什么不能直接用 LAG() 或自连接判断前后行?
单纯比对当前行与上一行是否达标(如 score >= 80),只能知道“是否中断”,但无法把同一段连续区间归为一组——你需要的是“每段连续区间的起始 ID”或“组内序号”,而不仅是布尔结果。
常见错误现象:
- 写了 WHERE score >= 80 AND LAG(score) OVER (ORDER BY dt) >= 80,结果只返回中间的连续记录,漏掉每段的第一天;
- 自连接 t1.dt = t2.dt - INTERVAL '1 day' 在日期有缺失(如节假日无数据)、或跨月/年时容易出错;
- 忽略排序字段必须严格唯一(如多条同一天记录没加次级排序,ROW_NUMBER() 结果不稳定)。
实操建议:
- 确保排序字段(如 dt)在 ORDER BY 中唯一,否则加一个唯一列(如 id)作为第二排序键:ORDER BY dt, id;
- 达标条件必须明确写成布尔表达式,并提前用子查询或 CTE 过滤/标记,避免在窗口函数中混用逻辑;
- 不要试图用 COUNT(*) 直接套在 LAG() 条件上——它不产生分组。
组号偏移量算法的核心步骤
本质是利用“连续自然数序列的差值恒定”这一性质:对所有达标记录按时间排序后,ROW_NUMBER() 是连续的;若原始日期本身也连续,则 dt - ROW_NUMBER()(或等效的 dt - INTERVAL '1 day' * rn)在每段连续区间内为常量,这个常量就是“组号”。
实操建议:
- 先筛选出达标记录(比如 WHERE score >= 80),再计算窗口序号;
- 用 ROW_NUMBER() OVER (ORDER BY dt) 得到递增序号 rn;
- 构造组标识:MySQL/PostgreSQL 可用 DATE_SUB(dt, INTERVAL rn - 1 DAY) 或 dt - (rn - 1) * INTERVAL '1 day';SQL Server 用 DATEADD(day, 1 - rn, dt);
- 按该组标识 GROUP BY,再用 MIN(dt)、MAX(dt)、COUNT(*) 即可得每段连续区间的起止和长度。
示例(PostgreSQL):
WITH scored AS (
SELECT dt, score,
ROW_NUMBER() OVER (ORDER BY dt) AS rn
FROM records
WHERE score >= 80
),
grouped AS (
SELECT *,
dt - (rn - 1) * INTERVAL '1 day' AS grp
FROM scored
)
SELECT grp AS continuous_start,
MIN(dt) AS begin_dt,
MAX(dt) AS end_dt,
COUNT(*) AS days
FROM grouped
GROUP BY grp
ORDER BY begin_dt;
MySQL 5.7 或旧版本没有窗口函数怎么办?
不能用 ROW_NUMBER() 就没法做组号偏移量——这是该算法的前提。此时只能退回到变量模拟(风险高)或应用层处理。
变量方案虽可行,但极易出错:
- 必须确保 ORDER BY 在 SELECT 中显式声明,且执行计划不重排;
- 多线程并发查询可能导致变量状态混乱;
- MySQL 8.0 默认禁用变量赋值在 SELECT 中的副作用(sql_mode 含 ONLY_FULL_GROUP_BY 时会报错)。
实操建议:
- 如果必须用 MySQL 5.7,优先考虑在应用层读取有序达标记录,用 Python/Go 一次遍历打组;
- 若坚持 SQL,用自连接求“向前最近一个不达标日”,再用 DATEDIFF 算距今连续天数——但性能随数据量下降极快;
- 切勿在生产环境对 >10 万行数据用变量 + ORDER BY 模拟 ROW_NUMBER()。
真正难的不是写出那个 dt - (rn - 1) * INTERVAL '1 day' 表达式,而是确认你的 dt 字段没有重复、没有空缺、时区一致,并且所有达标记录已严格按业务意义排序——这些细节一旦错,组就全乱了。










