MySQL 8.0+ 应优先使用 ROW_NUMBER() 解决 Top-N 分组问题,需配合 PARTITION BY 分组和 ORDER BY 排序,避免误用 RANK/DENSE_RANK 或子查询导致的并列、NULL 值误判等错误。

MySQL 8.0+ 直接用 ROW_NUMBER() 窗口函数最稳妥
如果你的 MySQL 版本 ≥ 8.0,ROW_NUMBER() 是解决 Top-N 分组问题最直观、最可靠的方式。它按指定排序为每行分配唯一序号,天然支持“每个分组内排名”,不会因并列值导致跳名或重复名。
常见错误是误用 RANK() 或 DENSE_RANK()——它们处理并列时会生成相同排名(比如两个第1名后直接第3名),而“前三名”通常指物理上排在前三位的记录,不管分数是否并列。
- 必须写
PARTITION BY明确分组字段,否则整个结果集只算一个组 - 排序字段(
ORDER BY)要选好:比如按销售额降序,就写ORDER BY sales DESC - 别在外部
WHERE中直接过滤row_num ——窗口函数不能在同级 <code>WHERE中引用,得套一层子查询或 CTE
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS row_num
FROM products
)
SELECT id, name, category, sales
FROM ranked
WHERE row_num <= 3;
MySQL 5.7 及更早版本只能靠自关联或变量模拟排名
老版本没窗口函数,@row_number := @row_number + 1 看似简单,但极易出错:MySQL 不保证变量赋值顺序,尤其在 JOIN 或复杂 ORDER BY 下,排名可能错乱甚至重复。生产环境不建议依赖用户变量做 Top-N。
更稳的做法是自关联计数:对每条记录,统计同组中“排序值更大”的记录数,加 1 就是名次。虽然性能差(O(n²)),但逻辑确定、兼容所有版本。
- 关联条件必须同时满足:同组(
t1.category = t2.category)且更强(t2.sales > t1.sales) - 记得用
COUNT(t2.id)而非COUNT(*),避免NULL干扰计数 - 如果允许并列(如两个最高分都算第1名),改用
>=并配合DENSE_RANK逻辑,但此处“前三名”仍指最多三条记录,不是“排名 ≤ 3”
SELECT t1.id, t1.name, t1.category, t1.sales
FROM products t1
WHERE (
SELECT COUNT(*)
FROM products t2
WHERE t2.category = t1.category
AND t2.sales > t1.sales
) < 3;
用 LIMIT + 应用层拼接只适用于分组数少、数据量小的场景
有人想对每个 category 单独执行 SELECT ... ORDER BY sales DESC LIMIT 3,再在应用里合并。这在分组数 ≤ 10 且每组数据不多时可行;但一旦有上百个分组,就是上百次查询,网络和连接开销陡增,还容易触发连接池耗尽。
- 无法原子性保证:中间某次查询失败,结果就不完整
- 没法做跨分组统一排序或后续聚合(比如“所有前三名中销售额最高的产品”)
- 如果分组键是动态的(如来自另一个表的枚举值),还得先查出所有分组值,再循环查询,代码更重
注意 NULL 和排序稳定性对排名的影响
如果排序字段(如 sales)含 NULL,默认排序时 NULL 在最前(ASC)或最后(DESC),但具体行为取决于 SQL 模式。Top-N 结果可能意外包含或排除 NULL 行。
- 显式控制
NULL位置:用ORDER BY sales DESC, id ASC补充二级排序,避免因主键无序导致相同sales的行每次结果不一致 - 若业务要求忽略
NULL值,加WHERE sales IS NOT NULL,别指望排名函数自动跳过 - 老版本自关联写法中,
t2.sales > t1.sales会让t1.sales IS NULL的行永远满足子查询返回 0,从而被错误纳入结果——这是最容易被忽略的坑


















