在数据库查询中,经常遇到“按某个字段分组,再取每组里面排在最前面的几条记录”这类需求。比如按部门取工资最高的三人,按城市取气温最低的两天。使用ROW_NUMBER窗口函数可以把这类问题写得非常直观,而且大多数主流数据库都支持。

一、ROW_NUMBER窗口函数基本语法
ROW_NUMBER是一个窗口函数,它为查询结果集中的每一行分配一个唯一的连续整数,整数的分配规则由OVER子句里的PARTITION BY和ORDER BY决定。PARTITION BY用来划分子集,相当于分组;ORDER BY决定每个子集内部的排序方向。编号从1开始,同一个分区内不会重复。
基本写法如下:
SELECT
row_number() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn,
employee_id,
department_id,
salary
FROM employee;
上面这段SQL会把员工按部门分开,每个部门里工资从高到低编上号。有了这个序号,只需要外层再筛选rn小于等于N,就能拿到每个部门的前N条。注意ROW_NUMBER本身不做聚合,不会减少行数,它只是多算出一列编号。
二、查询每个分组前N条记录的完整写法
最常见做法是把窗口函数放在子查询或者公用表表达式(CTE)里,然后在外层加WHERE条件。以下示例取每个部门工资前三名的员工:
WITH ranked_emp AS (
SELECT
row_number() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn,
employee_id,
department_id,
salary
FROM employee
)
SELECT
employee_id,
department_id,
salary
FROM ranked_emp
WHERE rn <= 3
ORDER BY department_id, rn;
这段代码先通过CTE算出编号,再过滤rn小于等于3。逻辑清晰,也方便数据库优化器处理。如果不用CTE,也可以写成派生表:
SELECT
employee_id,
department_id,
salary
FROM (
SELECT
row_number() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn,
employee_id,
department_id,
salary
FROM employee
) t
WHERE t.rn <= 3;
两种写法在多数数据库里性能接近。使用CTE通常可读性更好,也更容易在复杂查询中复用中间结果。无论哪种方式,核心都是“先编号,后过滤”。
三、ROW_NUMBER与RANK、DENSE_RANK的区别
初学者容易混淆ROW_NUMBER、RANK和DENSE_RANK。假设某部门工资为9000、9000、8000、7000,ORDER BY salary DESC时:
- ROW_NUMBER无论值是否相同,都给1、2、3、4,相同工资谁排前取决于数据库实现或额外排序列。
- RANK会给两个9000都标1,下一个8000标3,出现名次跳跃。
- DENSE_RANK给两个9000标1,8000标2,名次连续不跳。
如果业务要求“前N条”严格限制行数,比如只要三行,就用ROW_NUMBER。如果要求“前N名”且并列都算,比如工资前三名可能返回四行,则用RANK或DENSE_RANK再配合过滤。以下示例展示RANK写法:
SELECT
employee_id,
department_id,
salary
FROM (
SELECT
rank() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rk,
employee_id,
department_id,
salary
FROM employee
) t
WHERE t.rk <= 3;
可以看到,只是把函数名换了,其它结构完全一致。选择哪个函数取决于业务对“并列”的处理规则,而不是性能差异,三者底层都是窗口排序。
四、常见错误与优化建议
一个常见错误是在WHERE里直接写row_number() OVER(...) <= N,这是不允许的,因为窗口函数不能出现在WHERE子句中,必须先算出再过滤。另一个错误是ORDER BY列不够唯一,导致同分区同值行的编号不稳定,每次执行可能顺序不同。解决方法是增加第二排序列,例如ORDER BY salary DESC, employee_id ASC。
在性能方面,PARTITION BY和ORDER BY用到的列最好有复合索引,例如对employee表的(department_id, salary DESC)建索引,能让数据库避免额外排序。数据量极大时,也可以考虑先按分区字段过滤再算编号,减少处理行数。以下为建索引示例:
CREATE INDEX idx_emp_dept_sal ON employee (department_id, salary DESC);
最后提醒,窗口函数不改变原表行数,也不会像GROUP BY那样折叠数据,因此它特别适合既要分组前N条、又要保留明细字段的场景。掌握ROW_NUMBER后,类似“最新订单”“最高分记录”等需求都能用统一模式快速解决。
SQLROW_NUMBER窗口函数修改时间:2026-08-01 10:42:25