← 返回题目列表

如何用 SQL 查询每组 Top N?

高频 中等 第 5 / 28 题 更新于 2026/07/29
SQLTop N窗口函数ROW_NUMBER

简化版

每组 Top N 通常用窗口函数:按分组字段 PARTITION BY,按排名规则 ORDER BY,生成 ROW_NUMBER/RANK/DENSE_RANK,外层过滤排名小于等于 N。

详细版

典型写法:

SELECT *
FROM (
  SELECT t.*,
         ROW_NUMBER() OVER (
           PARTITION BY category_id
           ORDER BY score DESC, id DESC
         ) AS rn
  FROM products t
) x
WHERE rn <= 3;

PARTITION BY category_id 表示每个分类单独排名,ORDER BY score DESC, id DESC 表示按分数从高到低,同分时按 id 兜底。ROW_NUMBER 会强行给每行唯一序号;如果要并列名次,可以考虑 RANKDENSE_RANK

面试中要讲清三点:每组 Top N 不是全局 LIMIT N;排序必须稳定;并列分数时要根据业务选择 ROW_NUMBER/RANK/DENSE_RANK

完整版教学

一、每组 Top N 和全局 Top N 不一样

全局 Top N 是整个结果集只取前 N 行。

SELECT *
FROM products
ORDER BY score DESC
LIMIT 3;

每组 Top N 是每个分组都取 N 行。例如 10 个分类,每个分类取前 3 个商品,最多返回 30 行。

分类 A -> Top 3
分类 B -> Top 3
分类 C -> Top 3

这类需求常见于“每个部门薪资前三”“每个分类销量前十”“每个用户最近三条记录”。

记忆钩子:全局 Top N 只有一个排行榜;每组 Top N 是每个分组各有一张排行榜。

二、窗口函数是最清晰写法

窗口函数可以在不压缩明细行的情况下,为每行计算组内排名。

SELECT *
FROM (
  SELECT e.*,
         ROW_NUMBER() OVER (
           PARTITION BY department_id
           ORDER BY salary DESC, id DESC
         ) AS rn
  FROM employees e
) t
WHERE rn <= 3;

执行逻辑:

按 department_id 分区
-> 每个分区内部按 salary DESC, id DESC 排序
-> 给每行编号 rn
-> 外层保留 rn <= 3

这样既保留员工明细,又能按部门分别取前三。

三、ROW_NUMBER、RANK、DENSE_RANK 怎么选

并列分数会影响结果数量,所以排名函数选择非常重要。

函数并列处理示例分数 100,100,90取 <=2 的结果
ROW_NUMBER每行唯一序号1,2,32 行
RANK并列同名次,跳号1,1,32 行
DENSE_RANK并列同名次,不跳号1,1,23 行

如果业务说“每个部门取 3 个人”,用 ROW_NUMBER。如果业务说“取排名前三,允许并列”,可能用 DENSE_RANKRANK 更符合语义。

代码示例:

SELECT *
FROM (
  SELECT e.*,
         DENSE_RANK() OVER (
           PARTITION BY department_id
           ORDER BY salary DESC
         ) AS rk
  FROM employees e
) t
WHERE rk <= 3;

四、排序规则必须稳定

如果用 ROW_NUMBER,同分数据的顺序会决定谁进入 Top N。只写 ORDER BY score DESC 时,同分行的先后可能不稳定。

ROW_NUMBER() OVER (
  PARTITION BY category_id
  ORDER BY score DESC, id DESC
)

这里 id DESC 是兜底排序键,让同分行也有确定顺序。比如第 3 名边界有 5 个同分商品,ROW_NUMBER 必须选出其中一部分,兜底键能让结果可复现。

如果业务要求同分都展示,就不要用 ROW_NUMBER 硬截断,而是换成 RANKDENSE_RANK

五、为什么 GROUP BY 不能直接解决

GROUP BY 会把每组压成一行,而 Top N 需要保留组内多行明细。

SELECT department_id, MAX(salary)
FROM employees
GROUP BY department_id;

上面只能得到每个部门最高工资,拿不到对应员工的完整信息,更拿不到前三名。

如果你再把 name 放进 SELECT,会遇到非分组列来源不明确的问题。窗口函数的优势是“不合并行,只给行加组内指标”。

GROUP BY:多行 -> 每组一行
窗口函数:多行 -> 仍是多行,但多一列排名

六、老版本数据库的替代方案

如果数据库不支持窗口函数,可以用相关子查询或自连接模拟,但可读性和性能通常较差。

一种思路是计算“比当前行分数高的行数”:

SELECT p.*
FROM products p
WHERE (
  SELECT COUNT(*)
  FROM products p2
  WHERE p2.category_id = p.category_id
    AND (
      p2.score > p.score
      OR (p2.score = p.score AND p2.id > p.id)
    )
) < 3;

这相当于保留组内排在前 3 的行。若每组有 1000 行,这种相关子查询可能很重,因此有窗口函数时应优先使用窗口函数。

七、常见误区与追问

  • 误区:LIMIT 3 就是每组前三。 LIMIT 默认作用于全局结果集,不会按组重置。
  • 误区:GROUP BY 可以直接返回每组前三条明细。 GROUP BY 会压缩行,不适合保留多条明细。
  • 误区:同分场景不用考虑。 同分会影响 Top N 边界,必须选择合适排名函数。
  • 误区:ROW_NUMBERRANK 完全一样。 ROW_NUMBER 不保留并列,RANK 会保留并列名次并跳号。
  • 追问:每个用户最近一条记录怎么写?ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC),取 rn=1
  • 追问:如何让结果稳定? 在排序中追加唯一键,例如 ORDER BY score DESC, id DESC

八、加强记忆

每组 Top N 的公式是:分区、排序、编号、过滤。PARTITION BY 定义“每组”,ORDER BY 定义“Top 的规则”,排名函数定义“并列怎么处理”,外层 WHERE rn <= N 才是真正取结果。