子查询不能直接识别“连续”,需用两层row_number()相减生成grp标识连续段,再通过group by grp筛选长度≥n的段;窗口函数不可用于where,必须封装为派生表或cte。

子查询本身不能直接识别“连续”,必须配合序号逻辑
SQL 没有原生的“连续出现”语义,所谓连续,本质是:同一值在按某列排序后,其行号(或时间戳、ID)构成等差数列。子查询可以辅助生成序号或做分组标记,但关键在于构造「可判断连续性」的中间状态。
常见错误是直接写 WHERE col IN (SELECT col FROM t GROUP BY col HAVING COUNT(*) >= N) —— 这只查“总出现次数 ≥ N”,完全不保证连续。
- 真正要筛连续记录,至少需要两层序号:全局行号(如
ROW_NUMBER())和按值分组的行号(如ROW_NUMBER() OVER (PARTITION BY col ORDER BY id)) - 两者相减得到的差值(
grp)相同,即代表同一连续段;再对grp分组计数,就能筛出长度 ≥ N 的段 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持窗口函数;若用旧版 MySQL(
用窗口函数 + 子查询实现标准解法(推荐)
以筛选 status = 'success' 连续出现 3 次的记录为例(按 id 排序):
SELECT id, status
FROM (
SELECT id, status,
ROW_NUMBER() OVER (ORDER BY id)
- ROW_NUMBER() OVER (PARTITION BY status ORDER BY id) AS grp
FROM logs
) t1
WHERE status = 'success'
AND grp IN (
SELECT grp
FROM (
SELECT status,
ROW_NUMBER() OVER (ORDER BY id)
- ROW_NUMBER() OVER (PARTITION BY status ORDER BY id) AS grp
FROM logs
) t2
WHERE status = 'success'
GROUP BY grp
HAVING COUNT(*) >= 3
);
说明:
- 内层子查询计算每个记录的
grp值:同一连续段内该值恒定 - 嵌套子查询(
SELECT grp FROM ... GROUP BY grp HAVING COUNT(*) >= 3)找出所有满足长度条件的段标识 - 外层用
IN回溯原始记录——这是子查询在此场景的核心作用:提供“段白名单” - 性能注意:若表很大,
grp计算和两次扫描可能慢;可考虑加WHERE status = 'success'提前过滤
WHERE 子句中不能直接用窗口函数,必须用派生表或 CTE
错误写法:WHERE ROW_NUMBER() OVER (...) - ROW_NUMBER() OVER (...) = ... 会报错,因为窗口函数不能出现在 WHERE 或 HAVING 中。
- 必须把带窗口函数的查询封装成子查询(即派生表),再在外层用普通列过滤
- CTE 更清晰,但部分老版本数据库不支持;若用子查询,别名不可省略(如上面的
t1) - 别试图用
EXISTS替代IN:只要子查询返回的是单列grp,两者性能差异通常可忽略;但IN更直观,且对 NULL 安全(若子查询可能为空,IN返回空集,EXISTS不影响主查询逻辑)
连续 N 次的边界容易漏掉首尾记录
上面解法返回的是整个连续段的所有记录,但有时你只需要“触发连续的那条”(比如第 3 条),或排除首尾(只取中间)。这时不能只改 HAVING COUNT(*) >= N,而要额外标记位置:
- 在派生表中增加
ROW_NUMBER() OVER (PARTITION BY grp ORDER BY id) AS pos - 若要取每段第 N 条,加条件
AND pos = N - 若要取每段最后一条,用
COUNT(*) OVER (PARTITION BY grp)得到段长,再比对pos = seg_len - 最容易被忽略的是:当 N=1 时,所有记录都满足“连续出现 1 次”,但业务上往往不认为这是“连续”——需单独处理或明确需求是否包含 N=1










