必须用ROW_NUMBER()窗口函数配合PARTITION BY实现,因GROUP BY+LIMIT会压缩行数、仅返回聚合结果而非每组多条原始记录,无法表达“每组排序后取前N”逻辑。

必须用 ROW_NUMBER() 窗口函数配合 PARTITION BY 实现,不能靠 GROUP BY + LIMIT 或普通子查询硬套——后者要么语法报错,要么逻辑错误(只取全局前N,不是每组前N)。
为什么不能用 GROUP BY + LIMIT
GROUP BY 会压缩行数,LIMIT 是在聚合后限制结果总条数,根本无法保留“每组内多条记录”。比如 SELECT category, MAX(score) FROM products GROUP BY category LIMIT 3 只返回 3 个分组的最高分,不是每个分组的前 3 条完整记录。
- 常见错误现象:
ERROR 1055 (SQLSTATE: 42000)(only_full_group_by 模式下字段未出现在 GROUP BY 中) - 即使绕过报错,也拿不到原始行数据(如 id、name、时间戳等)
- 想取“每组最新 3 条”,
GROUP BY根本无法表达“按时间排序后取前三”这个动作
ROW_NUMBER() 的标准写法结构
核心是两层嵌套:内层用 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 打序号,外层用 WHERE rn 过滤。
- 必须用子查询或 CTE 包裹,因为窗口函数不能直接出现在
WHERE子句中(执行顺序:WHERE → GROUP BY → 聚合 → 窗口函数 → ORDER BY → LIMIT) -
PARTITION BY category表示按category分组重置编号;ORDER BY score DESC决定组内排序方向 - 序号从 1 开始,所以取前 3 条写
WHERE rn ,不是 <code>
ROW_NUMBER()
- 如果你的环境是 MySQL 5.7,只能退回到相关子查询(性能差,N > 5 就明显变慢):
(SELECT COUNT(*) FROM products t2 WHERE t2.category = t1.category AND t2.score > t1.score) - 确认版本:运行
SELECT VERSION();,别依赖运维口头说的“应该升级了” - 云服务(如阿里云 RDS、腾讯云 CDB)默认可能仍是 5.7,需主动切换到 8.0 实例
取最新/最旧记录时的时间字段注意点
用时间字段排序取“最新”时,ORDER BY created_at DESC 是对的,但要注意:
- 如果存在毫秒级时间戳且索引没覆盖,
ORDER BY可能走全表扫描,加联合索引(category, created_at)能显著提速 - 多个记录时间完全相同时,
ROW_NUMBER()仍会强制分配不同序号(不稳定),若需确定性结果,追加二级排序字段,例如ORDER BY created_at DESC, id DESC - 别用
MAX(created_at)先查时间再关联——这是典型 N+1 查询反模式,数据量一上去就卡死
真正麻烦的不是写法本身,而是当业务要求“每组前 3 条,但要去重、要排除测试数据、还要按用户权限动态过滤”时,这些条件必须全部塞进内层查询里,稍不注意就会漏过滤或错排序。窗口函数不帮你自动处理业务逻辑,它只忠实地按你写的 ORDER BY 和 PARTITION BY 执行。











