MySQL 8.0+ 应优先使用 ROW_NUMBER()、RANK()、DENSE_RANK() 窗口函数实现排名,语义清晰、结果可靠;5.7及以下版本只能用用户变量模拟,但顺序不可控、易出错,应尽量避免。

MySQL 8.0+ 直接用 ROW_NUMBER()、RANK()、DENSE_RANK()
如果你的 MySQL 版本 ≥ 8.0,别折腾变量了,窗口函数是唯一干净、可预测、支持排序和分组的方案。它天然处理并列、跳名次、不跳名次等逻辑,语义清晰,执行计划也更可控。
常见错误现象:ORDER BY 写在窗口函数外导致排名乱序;漏写 PARTITION BY 导致全表误排;把 RANK() 当 DENSE_RANK() 用,结果出现“1,2,2,4”却以为该是“1,2,2,3”。
-
ROW_NUMBER():严格按顺序编号,无并列(1,2,3,4) -
RANK():并列则同名次,后续跳过(1,2,2,4) -
DENSE_RANK():并列同名次,后续不跳(1,2,2,3) - 必须配合
OVER (ORDER BY ...),括号内不能有子查询或非确定性函数(如NOW()) - 若需分组内排名,加
PARTITION BY user_id,注意它先分组再排序,不是先排序再分组
示例:按销售额降序排客户名次(并列不跳)
SELECT name, sales,
DENSE_RANK() OVER (ORDER BY sales DESC) AS rank_no
FROM customers;
MySQL 5.7 或更低版本只能靠用户变量,但必须按顺序扫描
变量方案本质是模拟“逐行累加”,依赖 ORDER BY 在查询执行时真实生效——如果优化器改写执行计划(比如用了索引覆盖但没按预期排序),变量就断掉,结果完全不可信。
常见错误现象:没加 ORDER BY 就直接赋值;在 WHERE 或 HAVING 中引用排名变量;把变量初始化和计算写在不同 SELECT 层级里(如子查询中初始化,外层用)。
- 变量必须在同一 SELECT 中初始化并更新:
@rank := @rank + 1和@rank := 0要出现在同一层级 - 必须显式
ORDER BY,且该字段最好有索引,否则 MySQL 可能不按你写的顺序读行 - 不能在 WHERE 中用排名变量(如
WHERE rank_no = 1),因为 WHERE 执行早于 SELECT 计算 - 若需分组排名,得用
IF(@group != type, @rank := 1, @rank := @rank + 1)+@group := type两步更新,顺序不能错
示例:兼容 5.7 的销售额降序排名(无并列)
SELECT name, sales,
@rank := @rank + 1 AS rank_no
FROM customers
CROSS JOIN (SELECT @rank := 0) AS _init
ORDER BY sales DESC;
ORDER BY 不生效?先查执行计划,再看是否被索引干扰
窗口函数或变量方案都卡在排序环节。很多人发现加了 ORDER BY sales DESC,结果还是乱的——大概率是 MySQL 用了某个索引(比如主键索引),跳过了你想要的排序路径。
使用场景:表有复合索引 (category, sales),但你想按 sales 单独排序;或者 sales 字段没索引,数据量大时优化器干脆放弃排序走全表扫描+文件排序(filesort),但并发下文件排序不稳定。
- 用
EXPLAIN看Extra列:出现Using filesort是正常;出现Using index却没按你预期排序,说明索引顺序覆盖了你的ORDER BY,得加FORCE INDEX或改写查询 - 对
sales字段建单独索引,尤其当它是排名主依据时 - 避免在排序字段上用函数,如
ORDER BY ABS(sales),会强制 filesort 且无法利用索引 - MySQL 8.0+ 中,窗口函数的
OVER排序不走索引优化,纯内存排序,大数据量要注意sort_buffer_size
变量方案在复杂查询中极易失效,能不用就别用
只要嵌套一层子查询、加个 JOIN、或套个 UNION,用户变量的行为就可能变成未定义。官方文档明确说:“变量赋值顺序不保证”,这意味着在不同版本、不同负载、甚至同一条语句执行多次,结果都可能不同。
性能影响:变量本身开销小,但为保顺序常被迫加 ORDER BY + 索引,而索引维护成本和排序内存占用在高并发下会放大。
- 不要在视图或存储过程中封装变量排名逻辑,调用方无法控制执行顺序
- 不要和
LIMIT混用:先 LIMIT 再排序?还是先排序再 LIMIT?变量根本不管这个,结果随机 - 替代思路:应用层排序(如 Python/Java 拿到数据后 rank)比硬扛 MySQL 变量更稳,尤其当结果集不大(
- 真要兼容老版本又需稳定排名,考虑生成临时表 + 自增 ID 模拟排名,虽然多一次写入,但行为确定
真正难的从来不是写出能跑的 SQL,而是写出在 1000 QPS、凌晨备份、主从延迟时依然返回一致结果的 SQL。










