窗口函数ROW_NUMBER()配合派生表或CTE才是可靠解法,因MySQL 5.7及更早不支持子查询中LIMIT,且WHERE...IN(SELECT...LIMIT 3)在8.0+中仅全局取前三、无法按category分别取前三。

子查询加窗口函数才是可靠解法
直接用 WHERE ... IN (SELECT ... LIMIT 3) 会出错——MySQL 5.7 及更早版本不支持子查询中用 LIMIT,即使在 MySQL 8.0+ 中,这种写法也无法按分类分别取前三,而是全局取前三后匹配,逻辑错误。
真正可行的是用窗口函数 ROW_NUMBER() 配合派生表或 CTE。它能对每个 category 独立编号,再外层筛选 rn 。
- 必须用
PARTITION BY category,否则编号跨分类混乱 -
ORDER BY sales DESC决定“前三”的依据,注意 NULL 值默认排最前,必要时加NULLS LAST(PostgreSQL)或用COALESCE(sales, 0)(MySQL) - MySQL 8.0+、PostgreSQL、SQL Server 2012+、Oracle 10g+ 支持该写法;SQLite 3.25+ 也支持,但旧版不支持
MySQL 8.0+ 实操写法(含去重与并列处理)
如果同一分类内多个商品销量相同且卡在第 3 名边界(比如第2、3、3、4名都是 120),用 ROW_NUMBER() 会强行拆成 2/3/4/5,漏掉并列的第3名;此时应改用 RANK() 或 DENSE_RANK()。
推荐用 DENSE_RANK():并列不跳号,更符合“销售前三”的业务理解。
SELECT category, product_name, sales
FROM (
SELECT
category,
product_name,
sales,
DENSE_RANK() OVER (
PARTITION BY category
ORDER BY sales DESC
) AS rn
FROM sales_table
) ranked
WHERE rn <= 3;没有窗口函数的老版本 MySQL 怎么办
MySQL 5.7 或更早版本不支持窗口函数,只能靠自连接或相关子查询模拟排名,性能差、写法绕,且容易因重复值或 NULL 出错。
- 自连接方式需统计“同分类中销量严格大于当前行的数量”,再 +1 得排名:
SELECT s1.category, s1.product_name, s1.sales FROM sales_table s1 WHERE (SELECT COUNT(*) FROM sales_table s2 WHERE s2.category = s1.category AND s2.sales > s1.sales) - 该写法在有并列销量时可能返回超过 3 行(例如四个商品销量并列第一,全部满足“大于它的数量
- 必须为
(category, sales)建联合索引,否则全表扫描极慢 - 不如升级 MySQL 版本或迁移到支持窗口函数的引擎
WHERE 条件不能下推到子查询内部
常见误区是把过滤条件(如 WHERE status = 'active')只写在外层,导致子查询先计算全部数据再过滤,效率低下甚至 OOM。
务必把业务约束提前加在子查询里:
SELECT category, product_name, sales
FROM (
SELECT
category,
product_name,
sales,
DENSE_RANK() OVER (
PARTITION BY category
ORDER BY sales DESC
) AS rn
FROM sales_table
WHERE status = 'active' -- ✅ 这里过滤,减少窗口计算量
) ranked
WHERE rn <= 3;窗口函数本身不支持 WHERE 下推,所以这个过滤位置是手动控制的关键点。
实际跑起来发现结果比预期多,大概率是没处理并列,或者分区键写错了字段名——检查 PARTITION BY 后面是不是真用了分类字段,而不是拼写成 caegory 这种低级错误。

















