SQL 窗口函数怎么用?ROW_NUMBER、RANK 和 DENSE_RANK 有什么区别?
下面是一段教学用的模拟面试。
🧑💻 面试官:每个班取成绩最高的两名学生,你会怎么写 SQL?
🙋♂️ 我:按班级分组,再按成绩排序,取前两名。
🧑💻 面试官:GROUP BY 之后,每个学生的明细还在吗?两个人并列第二名,你是取两个人,还是把并列的都留下?
🙋♂️ 我:要先确定“前两名”是人数还是名次,再选择排名函数。
🧑💻 面试官:那 ROW_NUMBER、RANK、DENSE_RANK 分别会给出什么结果?同分时,ROW_NUMBER 先选谁?
排名先问两件事:「保留多少行」和「怎样处理并列」。函数名字要放到这两个条件后面记。
面试速答(60 秒版)
窗口函数会在相关的一组行上计算,但不会像普通 GROUP BY 聚合那样,把这些明细压成一行。OVER 里可以通过 PARTITION BY 划分计算范围,通过 ORDER BY 指定范围内的顺序。
ROW_NUMBER 给每行一个连续序号,同分也分开编号;RANK 让同分并列,后面的名次会跳号;DENSE_RANK 也让同分并列,但后面的名次不跳号。
因此,每组严格取两行,可以使用 ROW_NUMBER,并补充同分时的稳定排序条件。要保留并列,则根据业务采用 RANK 或 DENSE_RANK。排名结果通常放进子查询或 CTE,再由外层筛选,不能在同一层 WHERE 里直接使用刚算出的排名。

知识点详解:一张成绩表,怎样得到三种“前两名”?
窗口留下学生,聚合留下班级
假设咱们有一张 scores 表,记录班级、学生编号和成绩。
如果按班级 GROUP BY,再计算 MAX(score),得到的是每个班的最高分。原来一个班的多名学生,已经被汇总成一个班级结果。这适合回答“各班最高分是多少”,却不直接回答“哪些学生排在前面”。
窗口函数的处理不一样。它保留每位学生这一行,在旁边增加一个计算结果,例如班内排名。读者既能看到学生是谁,也能看到他排第几。MySQL 窗口函数用法区分了这两种计算。
这里的“窗口”是参与当前计算的行范围,不是页面上的窗口,也不意味着查询结果天然已经按排名排好。
PARTITION BY 和 ORDER BY,各自决定什么?
PARTITION BY class_id 表示不同班级分别计算。一个班的第一名,不影响另一个班从第一名开始编号。
OVER 里的 ORDER BY score DESC,则表示每个班按成绩从高到低计算排名。
这和查询最外层的 ORDER BY 不是同一个职责。前者决定计算规则,后者决定最终结果展示顺序。想让页面稳定地按班级、名次显示,外层仍然要写清楚排序。

同样四个成绩,为什么会出现不同名次?
假设某班四个学生的成绩依次为 95、90、90、80。
| 成绩 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 95 | 1 | 1 | 1 |
| 90 | 2 | 2 | 2 |
| 90 | 3 | 2 | 2 |
| 80 | 4 | 4 | 3 |
ROW_NUMBER 关心这一行排在第几个位置,两位 90 分学生各占一个位置。
RANK 把两位 90 分学生都算作第二名,但他们占了两个位置,所以 80 分接着排第四。DENSE_RANK 只数不同的成绩档位,95、90、80 是三个档位,因此最后是第三名。排名函数定义给出了并列与跳号规则。
如果只按成绩排序,同分学生谁获得 ROW_NUMBER 的 2、谁获得 3,没有稳定保证。需要固定取舍时,可以约定同分按 student_id 升序排列。
不过,不能把这个唯一编号一并塞进 RANK 的排序,再期待同分仍然并列。它会让两行的排序键不再相同。
先算排名,再取结果
下面按 MySQL 8.4 语法,严格取每班两名学生;学生编号在同一班内唯一。
WITH ranked AS (
SELECT class_id, student_id, score,
ROW_NUMBER() OVER (
PARTITION BY class_id
ORDER BY score DESC, student_id ASC
) AS rn
FROM scores
)
SELECT class_id, student_id, score
FROM ranked
WHERE rn <= 2
ORDER BY class_id, rn;
内层给每名学生编号,外层才筛选 rn。这里使用 SQL,不需要再复制两份 TypeScript 和 Python 客户端代码来解释同一条查询。
如果需求是“成绩档位的前两档,同分全部保留”,将排名改成 DENSE_RANK,并且窗口排序只写 score DESC。对于上面的数据,会得到三名学生,而不是两名。
如果需求是竞赛名次意义上的前两名,用 RANK。两位学生并列第一时,下一位可能是第三,RANK <= 2 就只留下并列第一的两位。DENSE_RANK <= 2 则还会留下下一档成绩。
这正是为什么不能只听见“Top 2”,就立即选一个函数。

排名正确,不代表查询就一定便宜
假设 scores 已经积累很多年的成绩,但页面只展示本学期。应先在参与排名的数据中限定学期,而不是给所有历史学生编号以后,再从外层随意删掉旧数据。
但筛选条件放在哪里,也会改变排名口径。例如“全班排名后,只展示女生”和“只在女生中排名”,是两道不同的查询。不能为了少处理一些行,就把筛选条件挪到内层。
性能上再查看执行计划、参与行数、排序与临时结果。合适的索引可能帮助过滤或排序,但窗口计算不是写上 OVER 就自动免去这些开销。
面试官继续追问
RANK 和 DENSE_RANK 在没有并列时还有区别吗?
对于相同分区与顺序,没有并列时,名次会一致。它们的差别主要在并列以后是否跳号,所以测试数据里必须有同分。
每组前 N 行,直接 LIMIT N 不行吗?
普通查询末尾的 LIMIT 限制整份结果,不会自动给每个班各取 N 行。需要把每组的计算范围明确表达出来。
所有窗口函数都只看“前面几行”吗?
不是。分区、窗口排序和窗口帧是不同概念。排名函数与累计求和等函数,对帧的使用也不同;本题先讲排名,不能用它的直觉替代所有窗口函数规则。
面试速记卡
- 窗口函数:保留明细,在相关行范围上增加计算结果。
- PARTITION BY:各组独立计算;窗口 ORDER BY:决定计算顺序。
- ROW_NUMBER:每行不同号;RANK:并列且跳号;DENSE_RANK:并列不跳号。
- Top N:先确认行数、竞赛名次还是成绩档位。
- 稳定与口径:同分取舍要明确,筛选位置不能改变原来的问题。
公司面试真题
这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。
浏览公司面试真题 →