MySQL 8.0+ 应直接使用 ROW_NUMBER(),它基于逻辑排序生成连续序号,必须在 OVER() 中指定 ORDER BY(如 ROW_NUMBER() OVER (ORDER BY id)),缺则报错;分组编号需加 PARTITION BY;不可在同级 WHERE 中引用序号列,须用子查询或 CTE 包裹过滤。

MySQL 8.0+ 用 ROW_NUMBER() 最稳妥
如果你用的是 MySQL 8.0 或更高版本,ROW_NUMBER() 是最直观、最可控的方式。它按指定排序给每行编号,再用外层查询筛出编号 ≤ 3 的记录。
常见错误是直接在 WHERE 中写 ROW_NUMBER() OVER (...) —— 这会报错,因为窗口函数不能出现在 <code>WHERE 子句里。
- 必须用子查询或 CTE 包一层,比如:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn FROM products ) t WHERE rn <= 3;
-
PARTITION BY决定“每组”的划分依据,比如按category、user_id;ORDER BY决定组内排序逻辑,直接影响哪三条被选中 - 注意 NULL 值:如果
score有 NULL,默认排在最前(MySQL),可能把本不该进前三的记录顶上去,必要时加ORDER BY score DESC NULLS LAST(但 MySQL 目前不支持NULLS LAST,得用COALESCE(score, 0)之类兜底)
MySQL 5.7 及更早版本只能靠变量模拟
老版本没窗口函数,得用用户变量维护组内序号。但变量行为不稳定,尤其在复杂查询或加了 ORDER BY 后容易错乱,生产环境慎用。
典型症状是同一组出现重复序号、跳号,或者结果每次执行都不一样。
- 必须确保
ORDER BY和变量赋值顺序严格一致,例如:SELECT id, category, score FROM ( SELECT id, category, score, @rn := IF(@prev = category, @rn + 1, 1) AS rn, @prev := category FROM products, (SELECT @rn := 0, @prev := '') AS _ ORDER BY category, score DESC ) t WHERE rn <= 3; - 子查询里的
ORDER BY不能省——变量依赖这个顺序执行 - 不能加
LIMIT在外层,否则会截断分组过程,导致某些组根本没数据出来
PostgreSQL 和 SQL Server 直接用 ROW_NUMBER() 就行
这两个系统对窗口函数支持成熟,语法和 MySQL 8.0 一致,但细节有差异:
- PostgreSQL 允许在
ORDER BY里用NULLS LAST显式控制空值位置,比如ORDER BY score DESC NULLS LAST - SQL Server 支持
TOP (3) WITH TIES,但它是取“并列第3名及之前所有”,不是严格每组3条,容易误解 - 所有版本都要求
PARTITION BY字段必须出现在SELECT列表里(或至少能被识别),否则可能报错或结果异常
性能和索引怎么配才不慢?
无论哪种写法,如果没有合适索引,查 100 万行数据的“每组前3”可能秒变几十秒。
关键不是建单列索引,而是复合索引要覆盖 PARTITION BY 和 ORDER BY 字段。
- 例如按
category分组、按score DESC排序,最佳索引是:CREATE INDEX idx_cat_score ON products (category, score DESC); - 如果查询还带
WHERE status = 'active',要把status加到索引最左((status, category, score DESC)),否则索引可能失效 - 用
EXPLAIN看执行计划:确认是否用了索引、是否出现Using filesort或临时表——这些是性能瓶颈信号
窗口函数本身开销不大,慢基本都是因为没走索引或数据量太大。别急着换写法,先看执行计划。

















