group by 后不能直接用 limit 取每组 top n,因 limit 作用于全局结果;正确方法是用 row_number() 窗口函数配合 partition by 和 order by,再在外层筛选序号。

GROUP BY 后不能直接用 LIMIT 取每组 Top N
因为 LIMIT 作用于最终结果集,不是对每个分组生效。写成 SELECT ... GROUP BY category ORDER BY score DESC LIMIT 3,只会返回全局前 3 行,不是“每个 category 各取前 3”。这是最常踩的坑。
真正要的是“每组内独立排序、各自取前 N”,必须引入能表达“组内序号”的机制。
- 窗口函数(推荐):
ROW_NUMBER()+PARTITION BY是标准解法 - 老版本 MySQL(如 5.7)没窗口函数:得用变量模拟,但顺序必须靠子查询强制保证
- 极简场景(如只取每组 MAX/MIN):可用关联子查询或
IN (SELECT MAX() ... GROUP BY),但不通用
用 ROW_NUMBER() 实现分组 Top N 的正确写法
核心就三点:PARTITION BY 定分组、ORDER BY 定组内顺序、外层筛 rn 。缺一不可。
错误示例:ROW_NUMBER() OVER (ORDER BY score DESC) 漏了 PARTITION BY,结果是全表编号;WHERE rn 写在同一个 SELECT 层级会报错,因为 <code>rn 是窗口计算字段,还没生成。
-
PARTITION BY必须明确指定分组字段,比如PARTITION BY department -
ORDER BY必须写在OVER()里,不能写在外层ORDER BY(那是全局排序) - 必须套一层子查询或 CTE,才能在外部用
WHERE rn
示例(查每个部门薪资最高的 2 人):
SELECT dept_id, emp_name, salary
FROM (
SELECT dept_id, emp_name, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE t.rn <h3>RANK() 和 DENSE_RANK() 什么时候该换用?</h3><p>当分组内存在并列值(比如两个员工同为 15k),<code>ROW_NUMBER()</code> 仍会给不同序号(1,2),而业务可能要求“所有 15k 都算 Top 2”——这时就得换。</p>
- 要严格限制最多 N 行(哪怕并列也只留一个)→ 用
ROW_NUMBER() - 要包含所有并列者,且允许结果行数 > N → 用
RANK()(并列后跳号,如 1,1,3) - 要包含所有并列者,且排名连续(如 1,1,2)→ 用
DENSE_RANK(),但 Top N 场景少见
注意:RANK() 下 WHERE rn 可能漏掉第二个并列项——比如排名是 1,1,3,<code>rn=3 的记录会被过滤,但它的值和第 2 名一样。这时候得用 RANK() ,而不是硬比数值。
MySQL 5.7 没窗口函数怎么办?
只能靠用户变量模拟行号,但 MySQL 不保证变量赋值顺序,直接写 @rn := @rn + 1 在 SELECT 里大概率出错。
- 必须先用子查询把数据按分组和排序固定好顺序,再进变量计算
- 变量初始化要写在同一语句里,比如
(SELECT @rn := 0) init,不能靠上一条SET - 分组变量也要同步维护,比如
@dept := dept_id判断是否跨组
示例(每部门 Top 2,MySQL 5.7):
SELECT dept_id, emp_name, salary
FROM (
SELECT dept_id, emp_name, salary,
@rn := IF(@dept = dept_id, @rn + 1, 1) AS rn,
@dept := dept_id
FROM (SELECT * FROM employees ORDER BY dept_id, salary DESC) t,
(SELECT @rn := 0, @dept := '') init
) ranked
WHERE rn <p>变量方案脆弱、难维护,只要能升级到 8.0+,优先用 <code>ROW_NUMBER()</code>。真正容易被忽略的,是子查询里那个 <code>ORDER BY dept_id, salary DESC</code> ——它不是可选的,而是变量逻辑成立的前提。</p>











