连续达标天数的本质是识别不间断的达标时间序列,需用lag()判断状态变化并用sum() over构造分组id,而非简单求和或group by;关键在标记断点(如前后状态不同或首行)后累计生成段id,再按id聚合。

连续达标天数的逻辑本质是分组断点识别
连续达标不是简单求和,而是先识别「达标序列的起止」,再对每个序列算长度。关键在找出每次中断的位置:比如某天达标但下一天不达标,或某天不达标但下一天达标,这类边界点就是分组依据。
用 LAG() 或 LEAD() 比较当前行与相邻行的达标状态,生成一个「是否断开」标记;再用累计求和(SUM() OVER (ORDER BY ...))把这个标记转成分组 ID——相同 ID 的行就属于同一段连续区间。
常见错误是直接 GROUP BY user_id, is_pass:这只能分出「所有达标日」和「所有未达标日」两大块,完全丢失时间连续性。
用 LAG + 累计求和构造连续组 ID(推荐)
假设表 t_daily_check 有字段 user_id、check_date、is_pass(1 表示达标),按用户+日期排序后,对每个用户单独处理:
SELECT
user_id,
MIN(check_date) AS start_date,
MAX(check_date) AS end_date,
COUNT(*) AS days_count
FROM (
SELECT *,
SUM(is_break) OVER (PARTITION BY user_id ORDER BY check_date) AS grp_id
FROM (
SELECT *,
CASE
WHEN LAG(is_pass) OVER (PARTITION BY user_id ORDER BY check_date) != is_pass
OR LAG(is_pass) OVER (PARTITION BY user_id ORDER BY check_date) IS NULL
THEN 1
ELSE 0
END AS is_break
FROM t_daily_check
) t1
) t2
WHERE is_pass = 1
GROUP BY user_id, grp_id;
-
LAG(is_pass)取上一行达标状态,和当前比:不同即为断点(如前日未达标→今日达标,或反之) -
IS NULL处理首行,确保第一天必为新组起点 -
SUM(is_break) OVER ...是关键:把断点标记累加,自然形成递增的组 ID - 外层
WHERE is_pass = 1过滤掉未达标日,只统计「达标段」
自连接方案容易爆内存且难调试
自连接本质是枚举每对日期组合,判断是否构成连续达标起点和终点,典型写法:
SELECT a.user_id, a.check_date AS start_date, MAX(b.check_date) AS end_date
FROM t_daily_check a
JOIN t_daily_check b ON a.user_id = b.user_id
AND b.check_date >= a.check_date
AND NOT EXISTS (
SELECT 1 FROM t_daily_check c
WHERE c.user_id = a.user_id
AND c.check_date BETWEEN a.check_date AND b.check_date
AND c.is_pass = 0
)
WHERE a.is_pass = 1
GROUP BY a.user_id, a.check_date;
问题很明显:
- 子查询
NOT EXISTS在大数据量下极慢,MySQL 尤其明显 - 没限定时间范围时,
a和b的笛卡尔积会爆炸 - 无法直接得到「最长连续天数」,还得嵌套一层
MAX(COUNT(...)) - 一旦日期有空缺(比如周末无数据),逻辑就错:它默认「只要没记录就算未达标」,而实际业务常要求「只看有记录的天」
窗口函数方案要注意 NULL 和排序稳定性
如果 check_date 不唯一(比如同天多条记录),LAG() 的行为不可控,必须加二级排序:
- 改用
LAG(is_pass) OVER (PARTITION BY user_id ORDER BY check_date, id),其中id是主键或唯一序号 -
is_pass字段若为NULL而非 0/1,!=判断会失效,应改用COALESCE(is_pass, 0) != COALESCE(LAG(...), 0) - PostgreSQL 支持
LAG(..., 1, 0)设置默认值,MySQL 8.0+ 也支持,但老版本需显式COALESCE - 分区键
PARTITION BY user_id必须存在,否则跨用户混算,结果全乱
真正麻烦的不是语法,而是业务定义:「连续」是否允许中间跳过节假日?是否以自然日为准还是仅统计工作日?这些得在过滤原始数据时就处理干净,别指望窗口函数自动理解业务规则。










