row_number()构造差值分组是最通用解法,需先过滤目标状态、严格排序且排除重复时间点;关键在于差值在连续序列中恒定,而非减法本身。

直接结论:用 ROW_NUMBER() 构造差值分组是目前最通用、可移植性最强的解法,但必须先过滤目标状态(如 'Win')、严格排序、且不能容忍重复时间点。
为什么 ROW_NUMBER() 减日期/序号能分出连续段
关键不是“减法本身”,而是减出来的结果在连续序列中恒定。比如运动员连胜三场:'2022-01-17'、'2022-01-18'、'2022-01-25' —— 注意,这三天并不连续,所以不能直接用日期减 ROW_NUMBER()。
真正有效的前提是:你定义的“连续”基于某个**严格递增且无间隙的逻辑序号**(如比赛场次编号、自增 ID),或你已将原始数据按业务意义对齐(例如只取 result = 'Win' 的记录,并按 match_day 排序后重新编号)。
实操建议:
- 先
WHERE result = 'Win',把非胜利记录彻底排除,否则ROW_NUMBER()会把输/平局也计入序号,破坏连续性 - 再用
ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY match_day)得到每个选手胜场内的自然序号rn - 此时,若胜场日期本身连续(如
'2022-01-17','2022-01-18','2022-01-19'),则match_day - rn恒为'2022-01-16';一旦断开(如跳到'2022-01-25'),差值立刻变为'2022-01-22',自然分组 - 若日期不连续但你要按“胜场顺序”算连胜(即不管日期空档,只看胜场连着几场),那就不用日期,改用
match_day对应的序号列(如自增id或ROW_NUMBER()本身)作为基准,构造id - rn分组
MySQL / PostgreSQL / Hive 都支持的写法长什么样
以力扣题 2173. 最多连胜的次数 为例,标准解法需两层嵌套:
SELECT player_id, COALESCE(MAX(streak), 0) AS longest_streak
FROM (
SELECT player_id, COUNT(*) AS streak
FROM (
SELECT player_id,
match_day,
result,
ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY match_day)
- ROW_NUMBER() OVER (PARTITION BY player_id, result ORDER BY match_day) AS grp
FROM Matches
WHERE result = 'Win'
) t
GROUP BY player_id, grp
) t2
GROUP BY player_id;
说明:
- 内层两个
ROW_NUMBER()相减,本质是“全局胜场序号 − 当前胜场在所有结果中的序号”,只要胜场连续,这个差就恒定 -
WHERE result = 'Win'必须写在外层子查询之前,否则grp会被输/平局污染 -
COALESCE(MAX(streak), 0)是为了覆盖全没赢过的选手(如示例中player_id = 2),避免返回NULL - Hive 中
DATE类型支持直接减整数;PostgreSQL 要确保match_day是DATE(不是TIMESTAMP),否则需match_day::DATE - rn
最容易被忽略的三个坑
这三个问题不解决,结果一定错,而且很难一眼看出:
-
match_day字段是TIMESTAMP?必须先CAST(match_day AS DATE)或DATE(match_day),否则同一天多个胜场会导致ROW_NUMBER()分配不同序号,差值分裂 - 同一
player_id + match_day有多条胜场记录?必须提前去重,例如加GROUP BY player_id, DATE(match_day)或用DISTINCT ON (player_id, DATE(match_day))(PG) - 没加
ORDER BY match_day在窗口函数里?ROW_NUMBER()行为未定义,尤其当表无主键或索引时,每次执行结果可能不同
性能上,如果表很大,(player_id, match_day) 联合索引几乎是必须的——排序和分组都依赖它,否则 ORDER BY 会触发文件排序,慢得不可接受。











