不能靠 GROUP BY 原始字段识别连续性,必须用窗口函数构造连续段标识;最稳方法是 LAG() + SUM() 累计和或 ROW_NUMBER() 差值法,二者均要求显式、唯一排序(如 ORDER BY created_at, id),否则结果不可控。

直接说结论:不能靠 GROUP BY 原始字段,必须用窗口函数构造“连续段标识”,最稳的是 LAG() + 累计和,或 ROW_NUMBER() 差值法;两者都要求显式、唯一排序。
为什么 GROUP BY status 无法识别连续性
因为 GROUP BY status 只会把所有 'success' 归为一组,不管它们在时间轴上是否挨着。比如数据是 ['success','fail','success','success'],GROUP BY 统计出 success 出现 3 次,但连续最多只有 2 次——它完全丢失了顺序信息。
- 窗口函数(如
LAG()、ROW_NUMBER())依赖ORDER BY定义“前后”,这是识别连续的前提 - 没写
ORDER BY的LAG(status)返回结果不可控,尤其在 MySQL 8.0+ 中可能每次执行都不一样 - 若排序字段有重复(如多个记录同为
'2026-07-01 10:00:00'),必须追加唯一列,例如ORDER BY created_at, id
用 LAG() + SUM() 构造连续段标识(推荐初学者)
这是逻辑最直白、容错性较强的做法:先标记“是否新开一段”,再用累计和生成段 ID。
- 先写
LAG(status) OVER (PARTITION BY user_id ORDER BY created_at, id)拿上一行状态 - 构造标志位:
CASE WHEN status != LAG(status) OVER (...) OR LAG(status) IS NULL THEN 1 ELSE 0 END AS is_new_seg - 用
SUM(is_new_seg) OVER (PARTITION BY user_id ORDER BY created_at, id) AS seg_id得到每段唯一编号 - 外层
GROUP BY user_id, status, seg_id后COUNT(*)就是该段长度 - 注意:首行的
LAG()是NULL,所以OR LAG(...) IS NULL必须写,否则首段会被漏掉
用 ROW_NUMBER() 差值法构造连续段标识(性能更优)
适合大数据量场景,但对排序唯一性更敏感。核心是用两个 ROW_NUMBER() 相减,差值恒定即为同一连续段。
- 全局序号:
ROW_NUMBER() OVER (ORDER BY created_at, id) - 按值分组序号:
ROW_NUMBER() OVER (PARTITION BY status ORDER BY created_at, id) - 相减得段标识:
rn_all - rn_part AS grp—— 这个grp在 MySQL 中不能直接用于外层GROUP BY,得重复表达式或套一层子查询 - 必须确保
PARTITION BY status中的status不为NULL,否则所有NULL被归为同一段;如需排除,加WHERE status IS NOT NULL - 如果连续依据是日期,且字段是
TIMESTAMP,先用DATE(created_at)归一化,避免同一天多条记录被误拆
统计完连续段后,怎么取每个分组的最长段?
别在最外层用 LIMIT 1,那只会返回全局第一条。必须按业务维度(如 user_id)分别取 Top 1。
- MySQL 不支持
QUALIFY,得嵌套一层:ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY COUNT(*) DESC, status ASC),再筛rn = 1 - PostgreSQL 或 ClickHouse 可用
QUALIFY ROW_NUMBER() OVER (...) = 1,更简洁 - 如果同一
user_id有多个等长最长段(比如两个都是连续 5 天'success'),ROW_NUMBER()只返回一个;要全返回,改用RANK()并WHERE rk = 1 - 别忘了过滤单条记录的“伪连续段”:
HAVING COUNT(*) >= 2,除非业务真需要统计长度为 1 的段
真正容易被忽略的点是:连续性永远由你定义的 ORDER BY 决定,不是数据物理存储顺序;哪怕表里 id 是自增的,只要查询没写 ORDER BY id,窗口函数就可能乱序。别信“看起来没问题”,得验证首尾时间戳是否真连续。

















