用窗口函数识别连续重复值需按排序列计算行号差值构造连续段标识(grp),相同grp值的行属于同一连续块,再统计各grp长度筛选≥指定次数的连续段。

用窗口函数识别连续重复值的逻辑
连续重复不是指表里有相同值,而是某列在按特定顺序排列后,相邻行的值完全一致。比如按 id 或 created_at 排序后,status 连续三行都是 'pending' —— 这才算“连续重复”。直接 GROUP BY 会打散顺序,必须依赖排序 + 行号差值法或窗口函数。
关键思路是:给每行打一个“组内序号”,再和全局序号做差,相同差值的行就属于同一段连续块。
推荐做法:用 ROW_NUMBER() 配合分组序号相减,构造连续段标识(俗称“gaps-and-islands”解法)。
PostgreSQL / SQL Server / BigQuery 中的标准写法
以查找连续 3 次及以上 state 为 'error' 的记录为例:
SELECT id, state, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY id)
- ROW_NUMBER() OVER (PARTITION BY state ORDER BY id) AS grp
FROM logs
WHERE state = 'error'
) t
WHERE grp IN (
SELECT grp FROM (
SELECT grp, COUNT(*) c FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY id)
- ROW_NUMBER() OVER (PARTITION BY state ORDER BY id) AS grp
FROM logs
WHERE state = 'error'
) _
GROUP BY grp HAVING COUNT(*) >= 3
) _
);
- 外层过滤只保留属于“长度 ≥3 的连续段”的行
- 内层子查询先筛出目标值(如
state = 'error'),再计算grp—— 这个差值在连续段内恒定 - 注意:ORDER BY 必须明确,否则
ROW_NUMBER()结果不可靠;若时间字段有重复,需加二级排序(如ORDER BY created_at, id)
MySQL 8.0+ 同样适用,但低版本需改用变量模拟
MySQL 5.7 及更早不支持窗口函数,得用用户变量:
SELECT id, state, created_at
FROM (
SELECT *,
@grp := IF(@prev = state, @grp, @grp + 1) AS grp,
@prev := state
FROM logs
CROSS JOIN (SELECT @grp := 0, @prev := '') AS _
ORDER BY id
) t
WHERE state = 'error' AND grp IN (
SELECT grp FROM (
SELECT grp, COUNT(*) c
FROM (
SELECT *,
@grp := IF(@prev = state, @grp, @grp + 1) AS grp,
@prev := state
FROM logs
CROSS JOIN (SELECT @grp := 0, @prev := '') AS _
ORDER BY id
) _
WHERE state = 'error'
GROUP BY grp HAVING COUNT(*) >= 3
) _
);
- 变量赋值顺序敏感:
@grp必须在@prev更新前计算 - 必须显式
ORDER BY,否则变量行为未定义 - 不能在同一个查询中既赋值又读取(如
WHERE @prev = state),会出错
容易被忽略的边界情况
连续性判断高度依赖排序依据。如果业务上“连续”是按时间,但 created_at 存在毫秒级重复或时区混乱,ROW_NUMBER() 可能随机分配顺序,导致本应连续的记录被拆开。
- 务必检查排序字段是否严格单调(可用
COUNT(*) vs COUNT(DISTINCT created_at)快速验证) - 若存在并行插入、时钟回拨、批量导入等场景,建议增加唯一递增列(如自增
id)作为主排序依据 - WHERE 条件要放在窗口函数外层或子查询最里层——放错位置会导致
PARTITION BY统计范围错误 - 连续 2 次和连续 3 次的逻辑差异很大:连续 2 次可能大量误报,建议结合业务设定最小长度阈值(如 ≥3 或 ≥5)
实际执行前,先用 SELECT *, ROW_NUMBER()... 把中间结果查出来看几行,比硬调 SQL 更快定位断点在哪。










