必须用窗口函数row_number()在分组内排序编号再过滤:先partition by category按类分组,再order by sales desc组内降序编号,最后取rn≤3;若需处理并列,改用rank()或dense_rank();mysql 8.0+等新版支持,旧版需复杂模拟。

用窗口函数 ROW_NUMBER() 排序后过滤
直接在分组后取前三,不能靠 GROUP BY + LIMIT 实现——那只会返回每个分组的任意三条,不保证是销量最高。必须先按分类内销量降序编号,再筛出编号 ≤ 3 的记录。
关键点在于:排序必须在分组“内部”进行,ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) 中的 PARTITION BY 划分分类,ORDER BY 决定组内顺序。
- 如果销量相同,
ROW_NUMBER()会强制给出不同序号(比如并列第一会标成 1 和 2),需要并列时改用RANK()或DENSE_RANK() - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;老版本 MySQL(
5.7及更早)不支持窗口函数,得用自关联或变量模拟,复杂且易错 - 示例语句:
SELECT category, product_name, sales FROM ( SELECT category, product_name, sales, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn FROM sales_table ) t WHERE rn
RANK() 和 DENSE_RANK() 的区别在哪
当同一分类下多个商品销量相同时,三种排名函数行为不同:ROW_NUMBER() 严格递增,RANK() 跳过后续名次,DENSE_RANK() 不跳过。
比如销量为 [100, 90, 90, 80]:ROW_NUMBER() → [1,2,3,4];RANK() → [1,2,2,4](两个第二,下一个直接第四);DENSE_RANK() → [1,2,2,3](两个第二,下一个第三)。
- 业务上要“最多取前三名”,且允许并列(如两个第一、一个第三也算前三),用
RANK() - 要“恰好三条记录”,不管是否并列,只能用
ROW_NUMBER() -
DENSE_RANK()适合做“前 N 个梯队”,比如销量前 3 档,每档内销量相同
性能注意:排序字段要有索引
PARTITION BY category ORDER BY sales DESC 这个组合排序,如果没有对应索引,大数据量时会触发文件排序(Using filesort),速度骤降。
- 最优索引是复合索引:
CREATE INDEX idx_cat_sales ON sales_table (category, sales DESC); - 仅对
category建索引不够,因为组内排序仍需扫描全组数据 - 如果
sales是计算字段(如quantity * price),无法直接建索引,考虑物化该列或改用覆盖索引加表达式索引(PostgreSQL 支持,MySQL 8.0+ 部分支持)
老版本 MySQL(5.7 及之前)怎么绕过窗口函数限制
没有 ROW_NUMBER() 就得用变量或自连接模拟,但变量行为不稳定(执行计划变更、并发查询可能错乱),自连接又容易笛卡尔爆炸。
- 较稳妥的做法:用子查询统计“比当前销量高的同分类商品数”,再判断是否小于 3:
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 )
- 这个写法逻辑清晰,但性能差——每行都要跑一次子查询,数据量一过万就明显卡顿
- 真要兼容老 MySQL 且数据量大,建议升级或在应用层分页聚合,别硬扛在 SQL 里
窗口函数本身不难,但排序依据是否稳定、并列如何定义、索引是否匹配,这三点漏掉任一个,结果就可能不对或慢得没法用。











