导读:本期聚焦于弥生美月创作的《如何获取数据库中前 N 名及其并列结果(含所有同分记录)》,敬请观看详情。排行榜只取前五名,却把同分的并列用户丢掉了,这是排名查询里最常见的坑。本文围绕SQL中获取前N名并保留所有并列记录这一需求,详细讲解窗口函数RANK、DENSE_RANK与ROW_NUMBER的区别,给出MySQL 8.0、PostgreSQL及旧版本MySQL下的多种实现方案,包括自连接、相关子查询、INNER JOIN派生表等写法,并分析各方案在性能与可维护性上的差异,帮你选出最适合业务场景的排名查询写法。

做排行榜功能时,产品经理提出的需求往往是“取前5名”。但如果第5名和第6名分数相同,只取5条记录就会漏掉并列第5名的用户,这类用户投诉起来很难解释。本文围绕“获取前N名及其所有并列记录”这一经典SQL问题,给出多种实现方案,并分析它们在不同数据库版本下的适用性和性能表现。

如何获取数据库中前 N 名及其并列结果(含所有同分记录)

先理解三种排名函数的本质区别

要正确处理并列名次,首先要分清三个窗口函数的行为差异:ROW_NUMBER()RANK()DENSE_RANK()。三者的核心区别体现在遇到相同分数时如何处理名次。

ROW_NUMBER()对每一行都分配唯一的连续序号,即使分数相同也会强行区分先后,因此它天生不适合处理并列名次。RANK()对相同分数给出相同名次,并且下一个名次会跳过,例如两个并列第2名之后直接是第4名。DENSE_RANK()同样给出相同名次,但名次是连续的,两个并列第2名之后是第3名。处理“前N名含并列”的需求时,通常使用RANK(),因为它符合“并列第2、跳到第4”的常规竞赛直觉;而DENSE_RANK()会导致名次压缩,取“前3名”时实际可能包含更多名次的记录。

举个具体例子,假设成绩表中有分数 100、95、95、90,四种排名函数的结果如下:

SELECT score,
       ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
       RANK()       OVER (ORDER BY score DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY score DESC) AS drnk
FROM scores;
-- 结果:
-- 100 | 1 | 1 | 1
-- 95  | 2 | 2 | 2
-- 95  | 3 | 2 | 2
-- 90  | 4 | 4 | 3

可以看到,用RANK()过滤名次小于等于3时,会返回100、95、95三条记录,恰好是“前3名含并列”的语义;而用ROW_NUMBER()会丢掉其中一个95分,用DENSE_RANK()则会把90分也纳入“前3名”。选择哪个函数,取决于业务对“名次”的定义。

窗口函数方案:MySQL 8.0 与 PostgreSQL 的标准写法

MySQL 8.0 及以上版本、PostgreSQL、SQL Server、Oracle 都支持窗口函数,这是最简洁、可读性最好的方案。核心思路是先用RANK()计算名次,再在外层查询中过滤。

-- MySQL 8.0+ / PostgreSQL / SQL Server 通用写法
SELECT student_id, student_name, score
FROM (
    SELECT student_id, student_name, score,
           RANK() OVER (ORDER BY score DESC) AS rnk
    FROM exam_result
    WHERE exam_id = 1001
) AS t
WHERE rnk <= 5   -- 取前5名,含所有并列
ORDER BY rnk, student_id;

注意窗口函数不能直接写在WHERE子句中,因为WHERE的执行时机早于窗口函数的计算,所以必须借助派生表或CTE先算出名次再过滤。如果使用CTE,代码会更清晰:

WITH ranked AS (
    SELECT student_id, student_name, score,
           RANK() OVER (ORDER BY score DESC) AS rnk
    FROM exam_result
    WHERE exam_id = 1001
)
SELECT student_id, student_name, score
FROM ranked
WHERE rnk <= 5
ORDER BY rnk, student_id;

性能方面,窗口函数需要对排序键执行一次排序,整体复杂度约为 O(n log n)。当参与排名的行数很大时,建议在排序列上建立索引(例如exam_id + score的组合索引),让数据库走索引扫描来加速排序阶段。此外,如果只需要前N名而不关心完整名次,MySQL 8.0 的优化器在部分场景下还能利用索引避免全量排序,实际执行计划可以用EXPLAIN确认。

旧版本 MySQL 的替代方案:子查询与自连接

如果数据库是 MySQL 5.7 或更早版本,没有窗口函数可用,就需要借助经典写法。最常见的思路是:某行属于“前N名”,当且仅当分数比它高的去重分数个数小于N。

第一种写法是相关子查询,直接在WHERE中统计比当前分数更高的去重个数:

SELECT student_id, student_name, score
FROM exam_result e
WHERE exam_id = 1001
  AND (
      SELECT COUNT(DISTINCT e2.score)
      FROM exam_result e2
      WHERE e2.exam_id = e.exam_id
        AND e2.score > e.score
  ) < 5    -- 比它高的分数种类少于5,说明它在前5名内
ORDER BY score DESC, student_id;

这个查询的逻辑是:如果有4种不同的分数比95分高,那么95分的名次就是第5名,满足小于5的条件被保留。注意必须用COUNT(DISTINCT ...)而不是COUNT(*),后者会把并列记录也算进去,导致并列名次被错误地排除。这种写法的缺点是相关子查询对外层每一行都要执行一次,在数据量大且没有合适索引时性能较差。

第二种写法是先查出第N名的分数阈值,再用INNER JOIN回表筛选,通常性能更好:

SELECT e.student_id, e.student_name, e.score
FROM exam_result e
INNER JOIN (
    -- 找到前N名中的最低分数作为阈值
    SELECT DISTINCT score
    FROM exam_result
    WHERE exam_id = 1001
    ORDER BY score DESC
    LIMIT 5
) AS top_scores ON e.score = top_scores.score
WHERE e.exam_id = 1001
ORDER BY e.score DESC, e.student_id;

这个方案的思路分两步:内层查询用DISTINCT + LIMIT取出前5个不同的分数值,也就是确定“哪些分数属于前5名”;外层再把等于这些分数的所有记录全部取出来,自然就包含了所有并列记录。它的好处是内层子查询只需在去重后的分数集合上做LIMIT,扫描量小,配合(exam_id, score)组合索引效率很高。需要注意的是LIMIT语法在SQL Server中对应TOP,在Oracle中对应FETCH FIRST 5 ROWS ONLY,迁移时要相应调整。

方案对比与选型建议

三种方案各有适用场景,下面从可读性、性能和兼容性三个维度做个简单对比:

方案可读性性能兼容性
窗口函数 RANK最好,语义清晰需一次排序,大数据量依赖索引MySQL 8.0+、PostgreSQL等
相关子查询一般,逻辑较绕外层每行触发一次子查询,较差几乎所有数据库
DISTINCT + JOIN 回表中等,思路直观小集合回表,通常较好几乎所有数据库

选型时可以遵循几个原则:如果数据库支持窗口函数,优先使用RANK()方案,它语义最贴近业务描述,后续维护成本最低;如果是旧版本MySQL且排名基数(不同分数的个数)不大,推荐DISTINCT + JOIN方案;相关子查询方案适合数据量小、写快速脚本验证的场景,不建议在大表上使用。

最后提醒两个容易踩的坑:一是分页场景下的并列问题,如果用LIMIT 5 OFFSET 5翻页,并列记录可能被切断在页边界,正确做法是按“名次区间”分页而不是按物理行分页;二是排名依据列如果允许NULL,要明确ORDER BYNULL的排序位置(MySQL默认NULL最小,PostgreSQL默认NULL最大),必要时在业务层先过滤或用COALESCE兜底,避免并列判断出现意外结果。

SQL排名并列排名TOP N查询修改时间:2026-09-01 18:44:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。