ROW_NUMBER() 配合 PARTITION BY 可实现分组取 Top N:先按分组和排序生成行号,再在外层查询中筛选 rn ≤ N 的记录。

ROW_NUMBER() 分组取 Top N 的基本写法
直接用 ROW_NUMBER() 配合 PARTITION BY 是最可靠的方式,它给每个分组内的行按指定顺序编号,再用外层查询过滤 rn 即可。
常见错误是把 ORDER BY 写在窗口函数外(影响全局排序),或漏写 PARTITION BY(变成全表编号)。
- 必须写
PARTITION BY group_column,否则不是“每组内编号”而是全表连续编号 -
ORDER BY必须放在窗口函数的OVER()里,且要明确排序依据(如score DESC) - 不能在同一个查询层级用
WHERE rn —— 因为 <code>rn是窗口计算结果,需套一层子查询或 CTE
SELECT user_id, category, score
FROM (
SELECT user_id, category, score,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn
FROM orders
) t
WHERE t.rn <= 3;
和 RANK()、DENSE_RANK() 的关键区别
如果分组内存在并列值(比如两个用户 score 都是 95),ROW_NUMBER() 仍会强制分配不同序号,而 RANK() 会跳号,DENSE_RANK() 不跳号。选哪个取决于业务定义的“Top 3”是否允许并列。
- 要严格取最多 3 行(哪怕第 3 名有并列也只留一个),用
ROW_NUMBER() - 要取“分数排前 3 的所有用户”(可能返回 4 或 5 行),改用
RANK() -
DENSE_RANK()在中间有并列时更紧凑,但对 Top N 场景使用较少
错误示例:RANK() OVER (PARTITION BY category ORDER BY score DESC) 返回 1,1,3,3,5,此时 WHERE rn 会漏掉第二个 3 分的记录。
性能与索引优化要点
ROW_NUMBER() 是计算密集型操作,数据量大时容易慢,尤其当 PARTITION BY 字段基数高(比如百万级用户 ID)且没索引时。
- 确保
PARTITION BY列 +ORDER BY列组合上有复合索引,例如INDEX(category, score) - 避免在
OVER()中用表达式排序(如ORDER BY ABS(score)),会导致索引失效 - PostgreSQL 和 SQL Server 对窗口函数优化较好;MySQL 8.0+ 支持,但 5.7 不支持,别在旧版本硬套
MySQL 8.0+ 和 PostgreSQL 的兼容写法差异
语法主体一致,但 MySQL 对子查询别名要求更严格,PostgreSQL 允许更灵活的 CTE 写法。
- MySQL 8.0 要求子查询必须有别名(如上面例子中的
t),否则报错Every derived table must have its own alias - PostgreSQL 可用
WITH t AS (...)更清晰,且支持LATERAL配合复杂逻辑 - SQL Server 支持
TOP (3) WITH TIES替代RANK(),但不适用于ROW_NUMBER()场景
跨数据库迁移时,最稳妥的是坚持子查询 + 别名 + 显式 ROW_NUMBER(),别依赖方言特性。
真正麻烦的是分组字段本身含 NULL —— 大多数数据库把 NULL 视为独立分组,但行为不统一,上线前务必用真实 NULL 数据验证。

















