MySQL 8.0+ 直接用 ROW_NUMBER() 窗口函数实现组内排序,需配合 OVER(PARTITION BY ... ORDER BY ...) 使用,且必须用子查询或 CTE 封装后才能在 WHERE 中过滤 rn ≤ 3,不可在 WHERE 中直接引用窗口函数结果。

MySQL 8.0+ 用 ROW_NUMBER() 最直接
MySQL 8.0 开始原生支持窗口函数,ROW_NUMBER() 是最直观的解法:按分组排序后编号,再筛出编号 ≤ 3 的行。
常见错误是把 WHERE 放在窗口函数外层却没套子查询——窗口函数不能在 WHERE 中直接引用,必须先用子查询或 CTE 包一层。
- 写法必须是:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn FROM products) t WHERE t.rn -
PARTITION BY字段要和业务分组逻辑一致,比如按user_id取每人最近 3 笔订单,就写PARTITION BY user_id - 注意
ORDER BY里有相同值时,ROW_NUMBER()仍会强制给唯一序号(比如两个并列第1名,会标成 1 和 2),如果要并列不跳号,得换RANK()
PostgreSQL / SQL Server 同样适用 ROW_NUMBER()
这些数据库对窗口函数支持更早、更稳定,语法和 MySQL 8.0+ 完全一致,可直接复用上面的子查询结构。
但要注意 PostgreSQL 中若 ORDER BY 字段含 NULL,默认排在最前(NULLS FIRST),可能意外改变排名顺序。显式写 ORDER BY score DESC NULLS LAST 更可控。
- SQL Server 2005+ 就支持
ROW_NUMBER(),无需版本顾虑 - 所有字段别名(如
rn)必须在子查询里定义,外层才能过滤 - 如果分组键有复合条件(如
(region, year)),PARTITION BY里直接写region, year,不要加括号
旧版 MySQL(5.7 及以下)只能靠自连接或变量模拟
没有窗口函数时,ROW_NUMBER() 得手动模拟,自连接是最兼容的方案,但性能随数据量下降明显。
核心思路:对每条记录,统计同组中“排序字段更大(或相等)”的记录数,该数量 + 1 就是它的名次。但要注意去重逻辑和等值处理。
- 典型写法:
SELECT a.* FROM products a WHERE (SELECT COUNT(*) FROM products b WHERE b.category = a.category AND b.score > a.score) - 这个写法默认实现“并列不占位”(即两个最高分都是第1名,下一个才是第3名),如果要严格取最多3条,得改用
并配合 <code>score DESC, id ASC消除歧义 - 千万级表慎用——子查询对每行都执行一次,没索引会全表扫描;务必确保
(category, score)有联合索引
用 RANK() 或 DENSE_RANK() 替代时的取舍
当业务要求“分数相同则名次并列”,就不能用 ROW_NUMBER(),否则会把并列第一拆成第1和第2名。
RANK() 并列后跳号(1,1,3),DENSE_RANK() 并列后不跳号(1,1,2)。选哪个取决于“前3名”是否允许出现第4名的数据。
- 例如某组分数为 [100,100,95,95,90],用
RANK()会返回前4行(名次为 1,1,3,3),而DENSE_RANK()返回前5行(1,1,2,2,3) - 所以真正“取前3名”且接受并列,应优先用
DENSE_RANK();若明确只要最多3条记录,还是回到ROW_NUMBER()+ 限制数量 - 所有数据库中这三个函数行为一致,但 Oracle 早期版本对
DENSE_RANK()支持较晚,生产环境需确认版本
实际跑起来最容易被忽略的是排序字段的 NULL 处理和索引覆盖——没索引的 PARTITION BY + ORDER BY 组合在大表上可能慢几秒甚至几分钟。

















