直接答案
在 MySQL 8 中,可以用 ROW_NUMBER() OVER (PARTITION BY 分组列 ORDER BY 聚合结果 DESC) 做分组排名。为了避免别名不可见、执行顺序混乱和筛选窗口函数时报错,推荐使用 CTE:
WITH product_sales AS (
SELECT
category_id,
product_id,
SUM(amount) AS total_amount
FROM order_items
GROUP BY category_id, product_id
),
ranked_products AS (
SELECT
category_id,
product_id,
total_amount,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY total_amount DESC, product_id
) AS rn
FROM product_sales
)
SELECT category_id, product_id, total_amount
FROM ranked_products
WHERE rn = 1;
这段 SQL 的含义是:先得到每个分类下每个商品的销售总额,再在每个分类内部按销售总额从高到低编号,最后取每组第一名。
可以写在同一层吗?
如果只是展示排名,不需要在同一层过滤 rn,MySQL 8 的分组查询里也可以把 SUM(amount) 放进窗口函数的 ORDER BY:
SELECT
category_id,
product_id,
SUM(amount) AS total_amount,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY SUM(amount) DESC, product_id
) AS rn
FROM order_items
GROUP BY category_id, product_id;
实战建议
如果后面要写 WHERE rn <= 3、要复用 total_amount,或者团队里有人不熟悉窗口函数执行顺序,用 CTE/子查询版本更稳。
Top N:每个分组取前三名
WITH customer_product_spend AS (
SELECT
customer_id,
product_id,
SUM(quantity * unit_price) AS spend
FROM order_items
GROUP BY customer_id, product_id
),
ranked AS (
SELECT
customer_id,
product_id,
spend,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY spend DESC, product_id
) AS rn
FROM customer_product_spend
)
SELECT customer_id, product_id, spend, rn
FROM ranked
WHERE rn <= 3
ORDER BY customer_id, rn;
这里的 product_id 是稳定排序字段:当两个商品消费额相同,MySQL 仍能给出可重复的排名顺序。
ROW_NUMBER、RANK、DENSE_RANK 怎么选?
| 函数 | 并列时 | 适合场景 |
|---|---|---|
| ROW_NUMBER() | 强制给不同序号 | 每组只要一条记录,例如最高销量商品 |
| RANK() | 并列同名次,后续名次跳号 | 竞赛排名,例如 1、1、3 |
| DENSE_RANK() | 并列同名次,后续名次不跳号 | 报表排名,例如 1、1、2 |
常见错误
错误 1:在 WHERE 里直接用窗口函数别名
-- 错误:WHERE 看不到 SELECT 里的 rn
SELECT
category_id,
product_id,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY SUM(amount) DESC) AS rn
FROM order_items
GROUP BY category_id, product_id
WHERE rn = 1;
改法:把这层查询放进 CTE 或子查询,在外层筛选 rn。
错误 2:把窗口函数当成 GROUP BY 的替代品
窗口函数不会把多行合并成一行;SUM(amount) OVER (...) 会给每行返回窗口结果。要先按商品、分类汇总成一行,仍然需要 GROUP BY。
错误 3:没有处理并列排序
如果 ORDER BY total_amount DESC 出现并列,ROW_NUMBER() 的谁先谁后可能不符合业务预期。加上主键、日期等第二排序字段可以让结果稳定。
FAQ
MySQL 8 支持 ROW_NUMBER 吗?
支持。MySQL 8.0 提供窗口函数,包括 ROW_NUMBER()、RANK()、DENSE_RANK() 以及带 OVER 的聚合窗口函数。
搜索 query 中的 order by sum( 应该怎么落地?
把 SUM(...) 命名为 total_amount,再在外层窗口函数中 ORDER BY total_amount DESC。这样可读性和可维护性最好。
MySQL 5.7 怎么写?
MySQL 5.7 没有窗口函数。可以用自连接或相关子查询模拟排名,但 SQL 更复杂、性能也更难控制;生产项目建议升级到 MySQL 8。