登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

窗口函数做分组排名时,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');

每个部门的最高分只有一人,第二高分有两人。如果业务说“奖励每个部门两人”,并列时仍然只能选两行;如果业务说“奖励前两个成绩档位”,那么第二档的两人都要保留,每个部门会返回三行。

同一份排序,两个函数回答不同问题

成绩表、部门分区、分数排序、稳定排序列与ROW_NUMBER和DENSE_RANK的静态查询结构
图1:分组排名查询的静态结构说明图;同一个部门分区和分数排序分别连接 ROW_NUMBER 与 DENSE_RANK,不代表数据库执行流程。

MySQL 官方文档把 ROW_NUMBER() 定义为分区内当前行的编号:即使两行在窗口排序值上相同,也会得到不同编号。DENSE_RANK() 则把同序值视为并列,为它们分配相同名次,而且后续名次不留空档。

比较点ROW_NUMBERDENSE_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。它很适合“前两个等级”“前两个价格档”“前两个不同分数”这类需求,但不保证固定返回两条记录。

前两行和前两个档位,返回数量可能不同

ROW_NUMBER固定两行与DENSE_RANK保留并列档位后可变行数的静态关系
图2:Top N 结果语义的静态关系说明图;左侧强调固定两行,右侧强调保留两个分数档位时可能返回多行。

这正是我第一次在报表里踩坑的地方: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 在组内产生排名,再在外层按排名值过滤。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>