SQL 中”每个分组取前 N 条”是最高频的分析模式之一,标准解法是先在每个组内用窗口函数排名,再在外部过滤。核心结构永远三步:用 ROW_NUMBER()/RANK()/DENSE_RANK() 加 PARTITION BY 在组内编号,把结果包进 CTE 或子查询,最后 WHERE 名次 <= N。据 dbsyntax 的 Top-N 专题,这条”排名 + 过滤”的两步式是该模式的规范写法;CSDN 的窗口函数实战也将其列为”每组 Top3″的标准模板。在 MySQL 8.0 之前,这需要用相关子查询或用户变量硬写,窗口函数让写法变得直白。
一、为什么 LIMIT 解决不了
新手本能地想用 ORDER BY ... LIMIT N,但 LIMIT 是全局的——它只限制整个结果集的行数,无法”每个组各取 N 行”。据 codewithfimi 的提醒,LIMIT 只在”整表取前 N”这种简单场景够用;一旦叠加”按组分”,就必须靠 PARTITION BY 的窗口函数。
二、标准写法:ROW_NUMBER 取严格 N 条
当你要”每组恰好 N 行、并列随意”时,用 ROW_NUMBER():
WITH ranked AS (
SELECT product_id, category, product_name, revenue,
ROW_NUMBER() OVER (
PARTITION BY category ORDER BY revenue DESC
) AS rn
FROM products
)
SELECT product_id, category, product_name, revenue
FROM ranked
WHERE rn <= 3
ORDER BY category, rn;
OneUptime 的 MySQL 示例用同一结构取”每类目营收前 3 的产品”。ROW_NUMBER 保证每组最多 3 行,即使第 3 名有并列也只取 3 行(引擎挑其中若干)。
三、并列时该选哪个排名函数
三种函数在 Top-N 边界上的行为不同,直接决定返回行数(据 OneUptime 对照表):
| 函数 | 并列处理 | Top 3 且第 3 名 2 人并列时返回 |
|---|---|---|
| ROW_NUMBER() | 任意拆开、不跳号 | 恰好 3 行 |
| RANK() | 同名次、名次跳号 | 4 行(1,2,3,3) |
| DENSE_RANK() | 同名次、不跳号 | 4 行(1,2,3,3) |
- 严格 N 行 →
ROW_NUMBER()(默认选择)。 - 并列都该进前 N →
RANK(),但WHERE rnk <= 3可能返回超过 3 行。 - 取前 N 档(不关心行数,只要前 N 个名次层级) →
DENSE_RANK()。
CSDN 实战文章把这一区别总结为:你究竟要”固定取 N 条””按名次取前 N 名”还是”按档位取前 N 层”——三种语义对应三种函数。
四、务必加次级排序键打破平局
据 dbsyntax 与 CSDN 共同强调,ORDER BY 内的排序列若有重复,数据库会任意决定谁排前,结果可能每次不同、跨引擎不一致。要让”谁排第一”稳定可预期,补一个稳定键(通常是主键):
ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC, product_id ASC) AS rn
凡是”唯一确定谁排第一””每组最新一条””去重保留一条”这类需求,排序条件都应尽量写完整。
五、窗口函数不能写在 WHERE 里
这是最常见的报错点。据 codewithfimi 解释,SQL 执行顺序中 WHERE 先于窗口函数计算,排名列此时还不存在。因此必须把排名放在 CTE/子查询里算好,再在外部用 WHERE 过滤;部分引擎(DuckDB、Snowflake、BigQuery、Teradata)支持 QUALIFY 直接在窗口上过滤,但 PostgreSQL、MySQL 不支持,便携写法仍用 CTE。
-- QUALIFY 写法(仅部分引擎)
SELECT id, user_id, total
FROM orders
WHERE status = 'paid'
QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC, id) <= 2;
六、Top-N 叠聚合指标
当排序依据是 SUM/COUNT 等聚合值时,用两段 CTE:先聚合,再排名。codewithfimi 给出模式:
WITH monthly_sales AS (
SELECT region, salesperson_id, SUM(amount) AS total_sales
FROM sales GROUP BY region, salesperson_id
), ranked AS (
SELECT *, RANK() OVER (PARTITION BY region ORDER BY total_sales DESC) AS rnk
FROM monthly_sales
)
SELECT * FROM ranked WHERE rnk <= 5;
七、”每组最新一条”是 Top-N 的特例
把 N 设为 1 即”每组取最新/最优一条”,是去重保留场景的签名用法:
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn
FROM orders
)
SELECT id, user_id, total, created_at FROM ranked WHERE rn = 1;
常见问题(FAQ)
Top-N 能用 LIMIT 吗?
不能。LIMIT 是全局限制,无法按组各取 N 行;必须用 PARTITION BY 窗口函数。
用 RANK 取前 3 为何可能超 3 行?
并列同名次会一起进入结果,若第 3 名有并列,返回行数就会超过 3。
MySQL 5.7 能写吗?
不支持窗口函数,需用相关子查询或用户变量,建议升级到 8.0。