因为GROUP BY不保留组内顺序且无法指定取前N行;需用窗口函数ROW_NUMBER()配合PARTITION BY和ORDER BY生成序号,再通过外层查询WHERE筛选rn≤N。

为什么直接用 GROUP BY 无法拿到每组前 N 条
因为 GROUP BY 本身不保留组内行的顺序,也没法指定“取第 1 到第 N 行”。你写 SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn FROM products 是对的起点,但若紧接着加 WHERE rn ,会报错——多数数据库(如 MySQL 8.0 之前、PostgreSQL 早期)不支持在同一个查询层级里引用窗口函数别名。必须把窗口函数放在子查询或 CTE 里才能过滤。
ROW_NUMBER() 必须配合子查询或 CTE 才能过滤
这是最常卡住的地方。窗口函数计算发生在 WHERE 之后、ORDER BY 之前,所以不能直接在 WHERE 中用 rn。正确做法是:
- 用子查询包裹:外层查
SELECT * FROM (SELECT ..., ROW_NUMBER() OVER (...) AS rn FROM t) t2 WHERE t2.rn - 或用 CTE(更清晰):
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM employees ) SELECT * FROM ranked WHERE rn
- 注意:MySQL 8.0+、PostgreSQL 13+、SQL Server 2005+ 都支持;SQLite 3.25+ 也支持,但旧版不支持窗口函数
ROW_NUMBER() 和 RANK() / DENSE_RANK() 的区别直接影响“前 N 条”的含义
如果组内有并列值(比如两个员工工资都是 15000),ROW_NUMBER() 会强行给不同序号(1,2,3…),而 RANK() 会给相同值相同排名(1,1,3…),DENSE_RANK() 是(1,1,2…)。这意味着:
- 用
ROW_NUMBER():严格取“物理上排前 N 的行”,哪怕分数一样也会被截断 - 用
RANK():可能返回超过 N 行(比如前 3 名有 2 个并列第 1,则返回 4 行) - 业务上要“分数前 3 名的所有人”,该用
RANK();要“只取前 3 行”,才用ROW_NUMBER()
性能关键:PARTITION BY 和 ORDER BY 字段必须有索引
没有索引时,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) 可能触发全表扫描 + 临时文件排序,尤其数据量过百万后响应明显变慢。
- 推荐复合索引:
CREATE INDEX idx_user_created ON orders (user_id, created_at DESC); - 注意:MySQL 中
DESC在索引定义中仅从 8.0 起真正生效;PostgreSQL 支持ORDER BY ... DESC索引,但需显式声明 - 如果
PARTITION BY字段基数极高(比如 UUID),且每组数据极少,窗口函数开销其实不大;但若每组上千行,就得警惕内存使用(work_mem在 PostgreSQL 中需调大)
窗口函数不是银弹——当分组数极大(千万级分组)、每组又极小(平均 1–2 行)时,PARTITION BY 的哈希分组本身就有开销;这时候用关联子查询或 LATERAL JOIN 反而更快。别只盯着语法,先看执行计划里的 WindowAgg 节点耗时。










