必须用窗口函数ROW_NUMBER()配合PARTITION BY实现分组取前N条:先按PARTITION BY分组、ORDER BY组内排序并编号,再外层过滤rn≤N;GROUP BY混用窗口函数会报错,子查询LIMIT无法分组。

用 ROW_NUMBER() 配合 PARTITION BY 实现分组取前N条
直接结论:必须用窗口函数,不能靠 GROUP BY 或子查询加 LIMIT 硬套——后者要么报错,要么逻辑错。核心是先编号再过滤,编号必须在分组内独立重置。
常见错误现象:SELECT *, ROW_NUMBER() OVER (ORDER BY score DESC) FROM t GROUP BY category 会报错(MySQL 8.0+ 也不允许),因为 GROUP BY 和窗口函数混用时语义冲突;还有人写成 SELECT * FROM t WHERE id IN (SELECT id FROM t ORDER BY score DESC LIMIT 3),这根本没分组,只是全表前3条。
-
PARTITION BY决定“每组”的边界,比如按category或user_id分 -
ORDER BY在括号里写在OVER内部,控制组内排序依据,如ORDER BY created_at DESC - 别在
WHERE里直接引用ROW_NUMBER()别名——它不能在同级WHERE中使用,得包一层子查询或 CTE
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn
FROM products
) t WHERE t.rn <= 3;MySQL 8.0+、PostgreSQL、SQL Server 都支持,但 SQLite 不行
SQLite 直到 3.25.0(2018年)才支持窗口函数,但很多旧环境(比如 Android 系统内置 SQLite)版本卡在 3.19 甚至更早,ROW_NUMBER() 会直接报 no such function: ROW_NUMBER。
- 确认版本:MySQL 用
SELECT VERSION(),PostgreSQL 用SELECT version() - 替代方案(仅限 SQLite):用相关子查询模拟,但性能差、N 大了就卡,例如
(SELECT COUNT(*) FROM t t2 WHERE t2.category = t1.category AND t2.score > t1.score) - SQL Server 2005+ 支持,但注意旧版不支持
FILTER子句,别和聚合窗口函数混淆
ROW_NUMBER() vs RANK() vs DENSE_RANK():选哪个取决于重复值怎么算“第N条”
如果组内有并列(比如两个最高分都是 95),ROW_NUMBER() 会强行给不同序号(1, 2, 3…),而 RANK() 是(1, 1, 3…),DENSE_RANK() 是(1, 1, 2…)。你想要“最多取3条”,还是“分数排名前三的全部取”,结果完全不同。
- 要严格限制条数(比如每组只显示3个商品),用
ROW_NUMBER() - 要保留并列(比如所有 95 分都算第一,都要展示),用
RANK()或DENSE_RANK(),再配合WHERE rank -
RANK()跳号,DENSE_RANK()不跳号——比如分数为 100, 100, 90, 90, 80 时,RANK()结果是 1,1,3,3,5;DENSE_RANK()是 1,1,2,2,3
WHERE 过滤要放在窗口之后,ORDER BY 却可以放在外层
很多人想“先筛出活跃用户再分组取前3”,却把 WHERE status = 'active' 写在子查询外面,导致先编号再过滤,编号依据的是全量数据——结果可能某组编号为 1 的记录被 WHERE 干掉了,剩下的是原编号 4、5、6 的记录,实际取到的是组内第4–6条。
- 条件过滤(如状态、时间范围)务必放在子查询内部,即
ROW_NUMBER()计算之前 - 外层
ORDER BY只影响最终输出顺序,不影响编号逻辑,可以放心加 - 性能提示:如果表很大,
PARTITION BY字段没索引,ROW_NUMBER()会触发全表排序,很慢;建议在(category, score)上建联合索引
真正容易被忽略的,是「过滤时机」和「索引覆盖」——前者错一点,结果就偏了;后者不跟上,大表一跑就超时。










