窗口函数做分组排名时,ROW_NUMBER 与 DENSE_RANK 怎么选
来源:17golang原创
时间:2026-10-07 10:58:17 112浏览 收藏
选择其实只看业务问题:如果你要“每个分组固定取前 N 行”,用 ROW_NUMBER();如果你要“每个分组保留前 N 个名次档位,并列记录全部保留”,用 DENSE_RANK()。两者都在分区内排名,但对同分记录的处理完全不同。
我以前写部门 Top 2 时,最容易忽略的不是函数名,而是“Top 2”到底指两名员工,还是两个分数档位。前一种需求要求结果数量稳定;后一种需求必须尊重并列,返回行数可能超过 2。先把这句话问清楚,SQL 基本就选对了一半。
同分数据为什么最能看出差别
准备一张季度成绩表。每个部门都有两位员工同分,这样 ROW_NUMBER() 与 DENSE_RANK() 的差异会直接出现。
-- 建立部门季度成绩示例表
CREATE TABLE quarterly_scores (
id BIGINT PRIMARY KEY,
department VARCHAR(32) NOT NULL,
employee VARCHAR(32) NOT NULL,
score INT NULL,
submitted_at DATETIME NOT NULL
);
-- 插入两组带并列分数的原创示例数据
INSERT INTO quarterly_scores
(id, department, employee, score, submitted_at)
VALUES
(1, '研发', '安然', 98, '2026-09-30 10:00:00'),
(2, '研发', '博文', 95, '2026-09-30 09:00:00'),
(3, '研发', '晨曦', 95, '2026-09-30 08:00:00'),
(4, '研发', '东海', 90, '2026-09-29 17:00:00'),
(5, '销售', '方晴', 100, '2026-09-30 11:00:00'),
(6, '销售', '高远', 97, '2026-09-30 10:30:00'),
(7, '销售', '海宁', 97, '2026-09-30 09:30:00'),
(8, '销售', '佳音', 88, '2026-09-29 16:00:00');
每个部门的最高分只有一人,第二高分有两人。如果业务说“奖励每个部门两人”,并列时仍然只能选两行;如果业务说“奖励前两个成绩档位”,那么第二档的两人都要保留,每个部门会返回三行。
同一份排序,两个函数回答不同问题

MySQL 官方文档把 ROW_NUMBER() 定义为分区内当前行的编号:即使两行在窗口排序值上相同,也会得到不同编号。DENSE_RANK() 则把同序值视为并列,为它们分配相同名次,而且后续名次不留空档。
| 比较点 | ROW_NUMBER | DENSE_RANK |
|---|---|---|
| 同分记录 | 仍分配不同序号 | 分配相同名次 |
| 名次是否连续 | 每行依次递增 | 并列组之间连续 |
| 筛选前 N | 每组最多 N 行 | 每组可能超过 N 行 |
| 典型用途 | 去重、每组固定 Top N | 等级、榜单并列、分数档位 |
需要固定两行时用 ROW_NUMBER
如果报表版位、名额或后续批处理要求每个部门恰好两行,我会用 ROW_NUMBER(),并把可重复值之后的稳定排序列写完整。下面先按分数降序,再按提交时间升序,最后用主键兜底。
WITH ranked AS (
SELECT
id,
department,
employee,
score,
submitted_at,
-- 同分时先提交者优先,主键负责最终稳定排序
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY score DESC, submitted_at ASC, id ASC
) AS rn
FROM quarterly_scores
WHERE score IS NOT NULL
)
SELECT id, department, employee, score, submitted_at, rn
FROM ranked
WHERE rn
研发组会选 98 分的安然和 95 分中提交更早的晨曦;销售组会选 100 分的方晴和 97 分中提交更早的海宁。这里的“先提交者优先”只是示例规则,实际项目可以换成更新时间、业务优先级或唯一主键,但必须让规则和需求一致。
我觉得 ROW_NUMBER() 最大的好处不是“没有并列”,而是结果基数可控。它的代价也很明确:遇到同分时必须人为决定谁先谁后;如果业务认为同分绝对平等,这种截断就会丢掉一部分并列记录。
需要保留并列档位时用 DENSE_RANK
如果排行榜展示的是成绩等级,第二名有几个人就应该展示几个人,我会改用 DENSE_RANK()。关键细节是:窗口里的 ORDER BY 只能放定义“同一档位”的业务字段。若把 id 也放进去,每行排序组合都不同,并列就被拆散了。
WITH ranked AS (
SELECT
id,
department,
employee,
score,
submitted_at,
-- 名次只由分数决定,同分员工必须得到相同名次
DENSE_RANK() OVER (
PARTITION BY department
ORDER BY score DESC
) AS dr
FROM quarterly_scores
WHERE score IS NOT NULL
)
SELECT id, department, employee, score, submitted_at, dr
FROM ranked
WHERE dr
这时每个部门都返回三行:最高分一行,第二高分两行。DENSE_RANK() 的名次是 1、2、2、3,不会因为第二档有两人而跳到 4。它很适合“前两个等级”“前两个价格档”“前两个不同分数”这类需求,但不保证固定返回两条记录。
前两行和前两个档位,返回数量可能不同

这正是我第一次在报表里踩坑的地方:SQL 看起来都像“分组后取前两名”,但一个控制行数,一个控制档位。评审需求时可以直接问下面两句话:
- 同分时是否允许多返回几行?允许,就倾向
DENSE_RANK()。 - 下游是否要求每组最多 N 行?要求,就使用
ROW_NUMBER()并明确同分决胜列。
如果既要尊重并列,又必须限制总行数,就不能只靠一个排名函数解决。需要在业务层定义截断策略,例如先按档位选候选,再用容量、优先级或抽签规则二次选择。不要悄悄用主键拆散并列,却仍把结果描述为“同分同名次”。
几个容易误判的边界
没有窗口 ORDER BY 会怎样?
官方文档指出,没有 ORDER BY 时,ROW_NUMBER() 的编号顺序是不确定的;对 DENSE_RANK() 来说,没有排序时所有行都是 peers,也就是同一名次。排名查询应明确窗口内排序,不要把最终结果集的外层 ORDER BY 误当成窗口排序。
为什么不能直接在同一层 WHERE 里写 rn
窗口计算发生在 WHERE、GROUP BY 和 HAVING 之后。窗口函数可以出现在选择列表和查询级 ORDER BY 中,因此筛选窗口别名通常要放到 CTE 或派生表的外层。这也是上面两个示例都先构造 ranked 再过滤的原因。
NULL 分数怎么排?
MySQL 窗口排序里,NULL 在升序时排前、降序时排后。示例直接用 WHERE score IS NOT NULL 排除未评分记录,因为“未评分”通常不应进入成绩排名。如果业务要保留它们,应单独定义未评分展示区,而不是默认把它们当作最低分。
DENSE_RANK 的 ORDER BY 能加多个字段吗?
可以,但每增加一个字段,peer 的定义就更严格。只有所有排序表达式都相同的行才会并列。若业务名次只由分数决定,窗口中就只放分数;提交时间和主键应放在外层展示排序中。若业务明确规定“分数相同再按完成时长分档”,才把完成时长加入 DENSE_RANK() 的窗口排序。
我的选择清单
- 固定每组 N 行、选最新一条、组内去重:
ROW_NUMBER()。 - 保留前 N 个不同分数或等级、同分全部展示:
DENSE_RANK()。 ROW_NUMBER()的窗口排序补齐唯一决胜列,避免同分顺序漂移。DENSE_RANK()的窗口排序只保留真正定义档位的字段,别用主键破坏并列。- 窗口别名放到 CTE 或派生表外层过滤,外层
ORDER BY只控制最终展示。
对我来说,最实用的判断不是背函数定义,而是先写出结果基数:到底必须返回两行,还是允许第二档并列后变成三行。前者选 ROW_NUMBER(),后者选 DENSE_RANK();剩下的工作就是把分区字段、业务排序和稳定展示规则写准确。
延伸问题
RANK 与 DENSE_RANK 又有什么区别?
两者都让 peers 共享名次;RANK() 在并列后会留下名次空档,DENSE_RANK() 不留空档。若成绩序列是 100、97、97、88,二者分别给出 1、2、2、4 和 1、2、2、3。
每组取最新一条为什么更适合 ROW_NUMBER?
因为目标是每组只保留一行。按时间降序并用主键兜底后,筛选 rn = 1 可以稳定得到唯一记录;DENSE_RANK() 在时间相同的情况下可能保留多行。
分组排名能不能直接用 LIMIT?
普通 LIMIT 限制的是整个结果集,不会为每个部门分别计数。每组 Top N 需要先通过 PARTITION BY 在组内产生排名,再在外层按排名值过滤。
-
127 收藏
-
291 收藏
-
500 收藏
-
169 收藏
-
255 收藏
-
数据库 · MySQL | 16小时前 | MySQL · 数据库运维 · MySQL备份 MySQL Clone CLONE LOCAL DATA DIRECTORY 本地克隆 clone_status285 收藏
-
201 收藏
-
386 收藏
-
110 收藏
-
201 收藏
-
190 收藏
-
204 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习