MySQL 8.0+中ROW_NUMBER()必须同时指定PARTITION BY和ORDER BY,漏写PARTITION BY会导致全表误排,漏写ORDER BY则直接报错;取每组前N名须用子查询封装并外层WHERE过滤rn。

MySQL 8.0+ 用 ROW_NUMBER() 最稳
直接在子查询里加窗口函数,是目前最清晰、性能也相对可控的做法。关键不是“能不能”,而是“别漏了 PARTITION BY 和 ORDER BY”。
常见错误是写成 ROW_NUMBER() OVER (ORDER BY score DESC) —— 这会把全表当一组排,不是每组前三。
- 必须写成
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) - 如果分数相同还想稳定取前3,加个次要排序:比如
ORDER BY score DESC, id ASC - 外层一定要用
WHERE rn 过滤,不能在窗口函数里写 <code>LIMIT
SELECT category, name, score
FROM (
SELECT category, name, score,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC, id ASC) AS rn
FROM products
) t
WHERE rn <= 3;旧版 MySQL(5.7)靠自关联或变量模拟
没有窗口函数时,ROW_NUMBER() 得手动“数”。自关联更可靠,变量写法看似简洁但容易因执行顺序出错(尤其加了 ORDER BY 或用了 JOIN 后)。
自关联的核心逻辑:对每个产品,统计同组里有多少产品分数比它高;小于3个,就说明它是前3。
- 注意
和 <code> 的区别:<code>COUNT(*) 表示“最多有2个比它高”,即排第1–3名 - 关联条件必须包含
a.category = b.category,否则变成全表比较 - 性能差:数据量大时是 O(n²),别在百万级表上直接跑
SELECT a.category, a.name, a.score FROM products a WHERE ( SELECT COUNT(*) FROM products b WHERE b.category = a.category AND b.score > a.score ) < 3;
PostgreSQL / SQL Server 用 FETCH FIRST 不行?
FETCH FIRST 3 ROWS ONLY 是按整个结果集取前3,不是每组3条——这是初学者最常踩的坑。它和 LIMIT 3 一样,只作用于最终结果,不感知分组。
真想用标准语法,得配合 LATERAL(PostgreSQL)或 APPLY(SQL Server),本质还是为每组单独执行一次子查询。
- PostgreSQL 示例:
LATERAL (SELECT * FROM products p2 WHERE p2.category = p1.category ORDER BY score DESC LIMIT 3) - SQL Server 要用
CROSS APPLY,且子查询里不能有聚合,否则报错Cannot use an aggregate or a subquery in an expression used for the group by list - Oracle 用户注意:
ROWNUM必须套两层子查询才能正确限制每组数量,单层WHERE ROWNUM 会提前截断
NULL 和并列排名怎么处理?
如果同一组里多个记录分数完全相同,ROW_NUMBER() 会强行给不同序号,而 RANK() 或 DENSE_RANK() 可能导致实际返回超过3条(比如4人并列第1,RANK() 全是1,WHERE rnk 就会全中)。
- 要严格“最多3条”,必须用
ROW_NUMBER() - 要体现并列语义(如“前3名,含并列”),改用
RANK(),但得接受结果数可能 > 3 - NULL 默认排在最后(
ORDER BY score DESC时),若想让 NULL 当最高分,加NULLS FIRST(PostgreSQL/Oracle 支持,MySQL 不支持)
真实业务里,经常要加 COALESCE(score, -999999) 来统一 NULL 排序行为,这点容易被忽略。

















