MySQL 5.7 中可用用户变量模拟 ROW_NUMBER():先在子查询中 ORDER BY 排序,再用 @rownum := @rownum + 1 实现全局编号;分组编号需配合 @group 变量检测分组变化并重置计数,且赋值顺序和初始化必须严谨。

MySQL 5.7 里用 @ 变量模拟 ROW_NUMBER() 的基本写法
MySQL 5.7 不支持窗口函数,但可以用用户变量(@rownum)在排序后逐行计数,实现类似 ROW_NUMBER() OVER (ORDER BY ...) 的效果。关键在于变量赋值顺序和执行时机——必须确保排序完成后再计数,否则结果不可靠。
常见写法是把排序逻辑封装进子查询,再在外层对变量做自增:
SELECT @rownum := @rownum + 1 AS row_num, t.name, t.score FROM (SELECT name, score FROM students ORDER BY score DESC) t CROSS JOIN (SELECT @rownum := 0) r;
这里 ORDER BY 必须出现在子查询内;如果写在外层,MySQL 5.7 不保证变量递增顺序与排序一致。
为什么 @rownum := @rownum + 1 有时会全为 1 或乱序
根本原因是 MySQL 对用户变量的求值时机未定义(non-deterministic),尤其在没有显式排序约束或优化器重排时,变量可能被多次读取、跳过或提前初始化。
- 避免在同一个
SELECT中既读又写同一变量(比如SELECT @x, @x := @x + 1) - 不要依赖
GROUP BY或HAVING触发变量更新——它们不保证执行顺序 - 不能在
WHERE或ON条件中修改变量,MySQL 可能因索引下推跳过部分行 - 使用
CROSS JOIN初始化变量比SET @rownum = 0更可靠,因为能绑定到查询生命周期
按分组模拟 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)
要实现分组内编号(比如每个班级内按分数排名),得用两个变量:@group 记录当前分组键,@rownum 在组内累加。核心逻辑是“检测分组变化 → 重置计数”。
SELECT
name,
class,
score,
@rownum := CASE
WHEN @group = class THEN @rownum + 1
ELSE 1
END AS row_num,
@group := class
FROM (SELECT name, class, score FROM students ORDER BY class, score DESC) t
CROSS JOIN (SELECT @rownum := 0, @group := '') r;
注意:@group := class 必须放在 SELECT 列末尾(或至少在 CASE 之后),否则 CASE 里读到的是旧值;另外 ORDER BY 必须严格匹配 PARTITION BY 和 ORDER BY 字段顺序。
替代方案:自连接或子查询计数(更稳定但慢)
如果数据量小(
SELECT s1.name, s1.score, (SELECT COUNT(*) FROM students s2 WHERE s2.score > s1.score OR (s2.score = s1.score AND s2.id <= s1.id)) AS row_num FROM students s1 ORDER BY s1.score DESC, s1.id;
这个写法不依赖变量,结果确定,但时间复杂度是 O(n²),且需要唯一字段(如 id)打破并列时的歧义。真实场景中,建议优先调通变量方案,仅当遇到难以复现的乱序问题时才切到该方式。
真正难处理的是并发写入下变量状态污染,以及 mysqld 重启后未重置变量导致的错位——这些点容易被忽略,但一旦出问题很难排查。


















