最可靠方案是用row_number()窗口函数配合子查询:先按category分组、sales_amount降序编号,再筛选rn≤3的行;需明确partition by和order by,mysql 8.0+等主流数据库均支持。

用 ROW_NUMBER() 窗口函数实现每组 Top 3
直接结论:SQL 没有内置的「按组取前 N」语法,必须依赖窗口函数。最可靠、兼容性最好的方案是 ROW_NUMBER() 配合子查询或 CTE。
核心逻辑是先按类别分组、在组内按销量(或价格等指标)降序编号,再筛出编号 ≤ 3 的行。
-
ROW_NUMBER()保证严格排序且不跳号(比如并列第 2 名时,会标为 2 和 3,不是 2 和 2) - 排序字段必须明确指定,例如
ORDER BY sales_amount DESC;漏写ORDER BY会导致语法错误 - 分组字段(如
category)必须出现在PARTITION BY中,不能只写在外部GROUP BY - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 均支持;SQLite 3.25+ 也支持,但旧版不支持窗口函数
SELECT category, product_name, sales_amount
FROM (
SELECT category, product_name, sales_amount,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY sales_amount DESC
) AS rn
FROM products
) ranked
WHERE rn
<h3>遇到并列数据时该用 RANK() 还是 DENSE_RANK()?</h3>
<p>如果同一类别里多个商品销量相同,<code>ROW_NUMBER()</code> 会强行拆出不同名次,可能不符合业务预期——比如两个销量并列第一的商品,你希望它们都算「Top 1」,而不是一个第 1、一个第 2。</p>
-
RANK():并列名次后跳号(1,1,3,4…),适合强调“名次段落” -
DENSE_RANK():并列名次后不跳号(1,1,2,3…),更贴近“进前三”的语义(只要名次 ≤ 3 就入选) - 例如某类中销量为 [100,100,95,95,90],用
DENSE_RANK()会返回名次 [1,1,2,2,3],全部五条都满足 ≤ 3;而ROW_NUMBER()返回 [1,2,3,4,5],只取前 3 条
所以真正要“每个类别中排名前三的商品(含并列)”,应优先用 DENSE_RANK():
SELECT category, product_name, sales_amount
FROM (
SELECT category, product_name, sales_amount,
DENSE_RANK() OVER (
PARTITION BY category
ORDER BY sales_amount DESC
) AS dr
FROM products
) ranked
WHERE dr
<h3>WHERE 和 HAVING 都不能提前过滤 Top 3</h3>
<p>常见误区是试图在主查询加 <code>WHERE row_num 却忘了窗口函数不能在 WHERE 中引用——因为 WHERE 执行早于窗口计算,此时 <code>row_num</code> 还不存在。</code></p>
- 必须把窗口函数放在子查询或 CTE 中生成序号,再在外层用 WHERE 筛选
-
HAVING是针对聚合结果的过滤,和窗口函数无关,用在这里会报错或逻辑错乱 - 也不能在
GROUP BY后直接套LIMIT 3,那只会限制整个结果集总数,不是每组 3 条 - 某些方言(如老版本 MySQL)不支持子查询中用窗口函数,需升级或改用自连接模拟,但性能差、易出错
大数据量下性能关键点
当 products 表超百万行时,ROW_NUMBER() 或 DENSE_RANK() 可能变慢,瓶颈常在排序阶段。
- 确保
(category, sales_amount)有联合索引,能加速PARTITION BY + ORDER BY的扫描 - 避免在窗口函数中使用复杂表达式(如
ORDER BY UPPER(product_name)),会阻止索引使用 - 如果只需取 Top 3 且类别数不多,可考虑用
UNION ALL分别查每个类别再合并,配合单类别索引反而更快 - PostgreSQL 中可加
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW显式声明窗口帧,但对 Top N 场景无实质优化
真正容易被忽略的是:窗口函数的排序稳定性。如果排序字段存在大量重复值(比如很多商品 sales_amount = 0),数据库可能按任意顺序分配序号,导致每次查询结果不一致——务必在 ORDER BY 中加入唯一字段兜底,例如 ORDER BY sales_amount DESC, product_id ASC。










