因为GROUP BY不保留组内顺序且聚合函数仅返回标量,无法获取整行TopN数据;须用ROW_NUMBER()配合PARTITION BY分组排序并筛选编号≤N的行。

为什么直接用 GROUP BY 拿不到每组 TopN
因为 GROUP BY 本身不保留组内行序,聚合函数(如 MAX()、COUNT())只能返回标量结果,无法返回“第1名的整行数据”。想取每组销量最高的前3个订单,GROUP BY + ORDER BY + LIMIT 是无效组合——LIMIT 作用于整个结果集,不是每组。
用 ROW_NUMBER() 配合 PARTITION BY 是最通用解法
ROW_NUMBER() 是窗口函数,PARTITION BY 定义分组边界,ORDER BY 在组内排序,它会给每组内的行从 1 开始连续编号。只要筛出编号 ≤ N 的行,就得到每组 TopN。
常见错误:用 RANK() 或 DENSE_RANK() 替代 ROW_NUMBER() —— 它们会并列(比如两个第1名后接第3名),导致实际返回行数不可控;而 ROW_NUMBER() 严格保证编号唯一且连续,适合“取前N条”语义。
- 必须写
ORDER BY子句,否则ROW_NUMBER()报错(SQL 标准要求) - 排序字段建议有确定性,例如加
id作为第二排序键,避免因相同分数导致结果不稳定 - 别在
WHERE中直接引用窗口函数别名(如rn),需套一层子查询或 CTE
WHERE
- CTE 写法更清晰,但部分旧版 MySQL 不支持 CTE(需 8.0.1+)
- 子查询是兼容性最强的选择,所有支持窗口函数的数据库都可用
- 别用视图封装这种逻辑——视图不能参数化 N,每次改 TopN 都得重建
性能关键点:PARTITION BY 字段一定要有索引
PARTITION BY 本身不触发索引,但 ORDER BY 中的字段(尤其是主排序字段)若没索引,会导致全表扫描 + 文件排序,TopN 查询可能比全表还慢。
- 理想索引:复合索引
(category, sales DESC, id),覆盖PARTITION BY和ORDER BY字段 - 如果
category基数极高(如百万级用户 ID),而只查其中几组,可先用WHERE category IN (...)缩小范围再开窗 - 大数据量时慎用
ROW_NUMBER()取 TopN,它仍需对每组全部排序;若只要 Top1,MAX() OVER (PARTITION BY ...)+ 自连接可能更优
真正容易被忽略的是:PARTITION BY 的字段值分布是否倾斜。比如 99% 的数据落在一个 category 上,那这个组的排序开销会拖慢整体查询,此时需要业务侧拆分或加过滤条件。











