mysql 8.0+ 用 row_number() 与 count() 实现连续分组频次统计的核心是通过 id - row_number() 构造连续段id,再按其分组计数;需确保排序确定性、索引优化,并避免变量方案的不确定性。

MySQL 8.0+ 用 ROW_NUMBER() 和 COUNT() 实现连续分组频次统计
核心思路是:先给每条记录打上“连续段ID”,再按该ID分组计数。关键在识别连续性——同一分组内,若 id(或时间字段)是连续的整数,那么 id - ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY id) 的值就唯一标识一个连续段。
示例表 t_log 含字段 user_id, status, log_time,想统计每个 user_id 下连续相同 status 的最长段长度:
SELECT user_id, status, COUNT(*) AS cnt
FROM (
SELECT user_id, status, log_time,
log_time - INTERVAL ROW_NUMBER() OVER (
PARTITION BY user_id, status ORDER BY log_time
) SECOND AS grp_key
FROM t_log
) t
GROUP BY user_id, status, grp_key
ORDER BY user_id, cnt DESC
LIMIT 1 BY user_id, status;
注意:LIMIT 1 BY 是 ClickHouse 语法;MySQL 需用窗口函数二次排序取 Top 1,见下节。
MySQL 中取每个分组的最高连续频次:嵌套窗口函数
MySQL 不支持 LIMIT 1 BY,必须用两层窗口:外层对每个 user_id 按连续段计数结果排序,取 ROW_NUMBER() = 1 的行。
-
grp_id必须用确定性表达式生成,比如(log_time - INTERVAL rn SECOND)或基于序号差(推荐用UNIX_TIMESTAMP(log_time)转整型后再减) - 若
log_time有重复,需加id等唯一列进ORDER BY,否则ROW_NUMBER()结果不稳定 - 性能敏感时,确保
(user_id, status, log_time)有联合索引
兼容 MySQL 5.7 的变量方案:慎用但有时不得不写
变量方案本质是模拟窗口逻辑,在无窗口函数的老版本中可行,但极易因执行顺序、优化器重排而错乱,仅限离线小数据量场景。
关键陷阱:
- 必须用
SELECT子句中变量赋值(@prev := status),且所有变量引用和更新写在同一SELECT中 - 必须显式
ORDER BY,且该ORDER BY不能被优化器忽略(可加FORCE INDEX) - 初始化变量要放在
FROM子查询或JOIN中,不能只靠SET—— 因为子查询里变量不继承外部SET
典型失败写法:SET @cnt := 0; SELECT ..., @cnt := IF(@prev=status, @cnt+1, 1) ... —— 这在 MySQL 5.7 中行为未定义。
PostgreSQL / SQL Server 推荐用 LAG() + 递归标记
比起“序号差”,用 LAG() 判断当前行与前一行是否断裂更直观:
SELECT user_id, status, COUNT(*) AS seg_len
FROM (
SELECT *,
SUM(is_new_seg) OVER (PARTITION BY user_id ORDER BY log_time) AS seg_id
FROM (
SELECT *,
CASE WHEN LAG(status) OVER (PARTITION BY user_id ORDER BY log_time) = status
THEN 0 ELSE 1 END AS is_new_seg
FROM t_log
) t1
) t2
GROUP BY user_id, status, seg_id;
这个写法逻辑清晰、易调试,且各数据库对 LAG() 支持度高;但要注意 ORDER BY 必须包含唯一键,否则 LAG() 可能返回任意前一行。
连续性判断本身不难,难的是不同数据库对执行顺序、NULL 处理、变量作用域的隐式约定——写完务必用含重复值、空值、边界时间的数据集验证。










