SQL无法一键统计分组内最长连续值,须先用ROW_NUMBER()差值法标记连续段(ORDER BY created_at,id与PARTITION BY status ORDER BY created_at,id相减得grp),再按user_id和grp分组求长度,最后每user_id取最大cnt对应status。

直接说结论:SQL 无法用单个聚合函数“一键统计分组内连续出现次数最多的值”,必须拆解为两步——先识别所有连续段并标记长度,再对每个分组取连续长度最大者。关键不在“众数”,而在“最长连续段的代表值”。
怎么用 ROW_NUMBER() 差值法提取每段连续值和长度
核心是构造稳定、可分组的 grp 列:对全表按时间/ID排序生成 rn_all,再按值分组排序生成 rn_part,二者相减得到连续段标识。这个差值在段内恒定,值一变就重置。
- 排序字段必须唯一,否则
ROW_NUMBER()结果不确定;若有重复created_at,务必追加主键:ORDER BY created_at, id -
PARTITION BY value_col ORDER BY created_at, id中的value_col是你要判断“连续”的字段(比如status),不是分组维度(比如user_id) - 别名
grp必须用英文+下划线,避免某些引擎(如 Hive)解析失败;别写成连续组或grp_id - 示例片段:
ROW_NUMBER() OVER (ORDER BY created_at, id) - ROW_NUMBER() OVER (PARTITION BY status ORDER BY created_at, id) AS grp
如何按 user_id 分组,找各自最长的连续 status 值
不能先 GROUP BY user_id 再算连续——那会把不同用户的记录混在一起破坏顺序。必须先全局构造 grp,再按 user_id 和 grp 二次分组统计长度,最后在每个 user_id 下取最长段的 status。
- 外层必须用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY cnt DESC, status ASC)排序,status ASC是为了频次相同时结果确定 - 若同一
user_id有多个等长连续段(比如两个都是连续 5 次success),此法只返回一个;要全部返回,改用RANK()并筛选rn - 过滤掉单条记录的“伪连续段”:在中间层加
HAVING COUNT(*) >= 2,避免噪声干扰 - 注意 NULL:如果
status可为空,PARTITION BY status会把所有 NULL 归为一组,需提前WHERE status IS NOT NULL
为什么不能直接用 MODE() 或嵌套 GROUP BY
MODE() WITHIN GROUP 看似捷径,但它只统计“值出现频次最高”,完全不感知顺序——['A','B','A','A'] 的众数是 A,但连续最多只有 2 次;而 ['A','A','B','A'] 连续最多是 2 次 A,众数仍是 A,但含义已偏移。
- 嵌套
GROUP BY user_id, status再ORDER BY COUNT(*) DESC LIMIT 1:只返回全局第一条,根本没按user_id切分,语法上可能通过,逻辑上全错 - 用
LAG()比较相邻行只能判断“是否中断”,无法直接得出“本段多长”,还得配合累计求和或子查询,复杂度不降反升 - MySQL 8.0+、PostgreSQL、SQL Server 2012+ 都支持该差值法;旧版 SQL Server(2008)无窗口函数,只能自连接,性能差且难维护
真正容易被忽略的是排序字段的确定性——哪怕业务上认为 created_at 不会重复,数据库层面只要没唯一约束,就可能出现非预期的 ROW_NUMBER() 摇摆,导致同一连续段被错误切开。上线前务必用 SELECT created_at, COUNT(*) FROM t GROUP BY created_at HAVING COUNT(*) > 1 扫一遍。

















