ROW_NUMBER()是最可靠获取每组第N条记录的方法,需配合PARTITION BY和明确ORDER BY;旧版MySQL可用自关联或变量模拟,但各有缺陷;并列需求应改用DENSE_RANK();性能依赖(dept, salary DESC)复合索引。

用 ROW_NUMBER() 窗口函数最可靠
绝大多数场景下,ROW_NUMBER() 是唯一能稳定拿到“每组第 N 条”的方法。它按指定排序对每组内记录编号,再外层筛选 rn = N 即可。注意:必须有明确的 ORDER BY,否则编号无意义,结果不可复现。
常见错误是漏写 PARTITION BY 或把 ORDER BY 写成常量(如 ORDER BY 1),导致全表排号或报错。MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持。
示例(查每个部门薪资第 2 高的员工):
SELECT dept, name, salary
FROM (
SELECT dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn = 2;
MySQL 5.7 或更老版本怎么办?
没有窗口函数时,只能靠自关联或变量模拟排名。自关联写法通用但性能差,尤其数据量大时;变量方式快但依赖执行顺序,且在 MySQL 8.0+ 中变量行为已不推荐。
自关联要点:
- 用
LEFT JOIN自连接,条件为同组且“对方值 > 当前值” - 用
COUNT(*)统计比当前记录更大的数量,+1 即为排名 - 需确保排序字段组合唯一,否则并列时排名会跳号(例如两个相同最高薪,都算第 1 名,下一个就是第 3 名)
变量方式(仅限旧版 MySQL):
SET @rn := 0, @dept := '';
SELECT dept, name, salary FROM (
SELECT dept, name, salary,
@rn := IF(@dept = dept, @rn + 1, 1) AS rn,
@dept := dept
FROM employees
ORDER BY dept, salary DESC
) t WHERE rn = 2;
⚠️ 注意:该写法在子查询中变量赋值顺序不被标准保证,高并发或优化器改写时可能出错。
遇到并列情况要“取所有第 N 名”怎么办?
ROW_NUMBER() 是严格排序,相同值也强制分先后;如果需要“所有薪资第 2 高的人”(哪怕多人并列),得换用 DENSE_RANK()。
区别:
-
ROW_NUMBER():1, 2, 3, 4…(即使值相同也绝不重复) -
RANK():1, 2, 2, 4…(并列后跳号) -
DENSE_RANK():1, 2, 2, 3…(并列不跳号)
所以查“所有第 2 名”,应写 DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) = 2。别误用 RANK()——它会在并列后跳过后续名次,导致漏数据。
性能和索引怎么配合?
窗口函数本身不阻塞,但 PARTITION BY + ORDER BY 字段必须走索引,否则每组都要临时排序,IO 和内存开销剧增。
建议建复合索引:
- 按
PARTITION BY字段升序(如dept) - 再按
ORDER BY字段逆序(如salary DESC)
例如:CREATE INDEX idx_dept_salary ON employees (dept, salary DESC);。没这个索引时,百万级数据查每组 Top N 可能从毫秒变秒级。
另外,如果 N 很大(比如每组第 1000 条),ROW_NUMBER() 仍需扫描全部行再过滤,此时不如加 LIMIT 子查询或改用物化视图预计算。

















