row_number() 比 max() + group by 更适合取分组内完整记录,因后者无法获取极值所在行的其他字段,而前者通过窗口排序+外层过滤可精确返回整行;需配合联合索引、避免where中直接使用窗口函数,并注意旧版本兼容性。

为什么 ROW_NUMBER() 比 MAX() + GROUP BY 更适合取“分组内某条完整记录”
因为 MAX() 只能返回极值本身,拿不到同一行的其他字段(比如用户姓名、时间戳);而用窗口函数可以先标记每组极值所在行,再过滤出整行数据。
常见错误是直接在 GROUP BY 后写 SELECT name, MAX(score) —— 这会报错或返回随机 name(取决于 SQL 方言和 strict mode)。
正确思路是:用窗口函数生成排序序号,再用外层 WHERE 筛选序号为 1 的行。
-
ROW_NUMBER()严格排序,相同值也会被强分先后,适合“取第一条”场景 -
RANK()和DENSE_RANK()遇到并列会跳号/不跳号,适合“取所有并列最高分”时用 - 排序字段必须明确写全,比如
ORDER BY score DESC, created_at ASC,否则结果不可靠
PostgreSQL / MySQL 8.0+ 中 ROW_NUMBER() 的标准写法
窗口函数不能出现在 WHERE 子句里,所以必须套一层子查询或 CTE。别图省事写成 WHERE ROW_NUMBER() OVER (...) = 1 —— 这语法非法,会报 ERROR: window functions are not allowed in WHERE。
SELECT user_id, score, subject
FROM (
SELECT user_id, score, subject,
ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC, user_id ASC) AS rn
FROM scores
) t
WHERE rn = 1;
-
PARTITION BY subject是分组依据,等价于GROUP BY subject -
ORDER BY score DESC, user_id ASC决定了谁排第一:分数高优先,相同时取 user_id 小的(避免随机) - MySQL 5.7 或更早版本不支持窗口函数,必须改用自连接或变量模拟,性能差且易出错
SQLite 和旧版 MySQL 怎么绕过窗口函数缺失问题
没有 ROW_NUMBER() 时,最容易踩的坑是用相关子查询比大小,但遇到重复极值就会漏数据。比如 WHERE score >= ALL (SELECT score FROM scores s2 WHERE s2.subject = scores.subject) 看似合理,实际在多行同分时会返回全部,而非“只取一行”。
- 安全做法是加唯一性兜底:用
(score, user_id)联合比较,确保每组最多一条 - SQLite 3.25+ 支持窗口函数,但默认编译可能关掉,执行
SELECT sqlite_version(), sqlite_source_id();确认是否启用 - MySQL 5.7 建议升级,硬扛变量方案(如
@rn := IF(@subj = subject, @rn + 1, 1))在并发或优化器重排下结果不稳定
性能关键点:索引怎么配才不拖慢 OVER (PARTITION BY ... ORDER BY ...)
窗口函数不是魔法,PARTITION BY a ORDER BY b 最好有联合索引 (a, b),否则每次都要临时排序,大数据量时直接卡死。别指望单列索引 (a) 或 (b) 能生效。
- 如果
ORDER BY含多个字段,索引顺序必须完全一致,(subject, score DESC, user_id ASC)不能写成(subject, user_id, score) - PostgreSQL 中可用
EXPLAIN ANALYZE看是否用了WindowAgg节点及是否走索引;MySQL 8.0+ 看EXPLAIN FORMAT=TREE里的window_function行 - 分组键基数太高(比如上千万个不同
subject)时,窗口计算内存开销大,考虑提前物化中间结果
真正麻烦的从来不是语法怎么写,而是当 PARTITION BY 字段存在大量空值或隐式类型转换时,数据库可能悄悄跳过索引,还查不出原因。










