MySQL 5.7及更早版本可用用户变量模拟ROW_NUMBER():需在单SELECT中完成@rownum初始化与递增,ORDER BY必须置于语句末尾以保证顺序,严禁分离赋值与使用,且不可用于GROUP BY或嵌套查询中。

MySQL 5.7 及更早版本中用 @rownum 变量实现行号
在没有 ROW_NUMBER() 的老版本 MySQL 中,唯一靠谱的虚拟序号方案就是用户变量。它不是标准 SQL,但能在大多数实际场景中稳定工作——前提是严格按执行顺序写,且避免优化器重排。
常见错误是把变量赋值和使用写在不同字段里,比如:SELECT @rownum := @rownum + 1 AS rn, name FROM t, (SELECT @rownum := 0) r —— 这在某些版本会因优化器提前计算 @rownum 导致序号全为 1 或跳变。
- 必须把初始化和递增写在同一表达式中,推荐写法:
(SELECT @rownum := @rownum + 1) AS rn - 初始化子查询必须放在
FROM子句最左侧,并确保无 JOIN 干扰执行顺序 - 如果需要按某列排序后编号,
ORDER BY必须出现在整个语句末尾,不能在子查询里
SELECT (@rownum := @rownum + 1) AS rn, name, score FROM student, (SELECT @rownum := 0) r ORDER BY score DESC;
PostgreSQL 8.x 或 SQLite 等不支持窗口函数时的替代方案
这类数据库通常不支持用户变量,强行模拟序号只能靠自连接计数,性能差、写法绕,只适用于小表(
核心思路是:对每一行,统计“排序值小于等于它的行数”。例如按 score 降序编号,就数有多少行的 score >= 当前行 score,再处理并列情况。
- 必须有明确的排序依据,且该字段最好有索引,否则自连接会 O(n²) 扫描
- 存在重复值时,相同
score会得到相同序号(类似RANK()),若要强制唯一需叠加主键比较 - SQLite 中
ROWID可辅助去重,但不能直接代表逻辑顺序
SELECT
(SELECT COUNT(*) FROM student s2
WHERE s2.score > s1.score
OR (s2.score = s1.score AND s2.id <= s1.id)) AS rn,
s1.name,
s1.score
FROM student s1
ORDER BY s1.score DESC, s1.id ASC;变量方式在 LIMIT 分页时的陷阱
很多人想用变量序号做分页,比如“取第 11–20 行”,结果发现 LIMIT 和变量递增顺序冲突,序号错乱甚至中断。
根本原因是:MySQL 在有 LIMIT 时可能先截取数据再执行变量赋值,导致 @rownum 只加了 10 次而不是 20 次。
- 绝对不要在含
LIMIT的外层直接用变量生成序号 - 正确做法是先用子查询生成完整序号集,再对外层结果
LIMIT,例如嵌套一层:SELECT * FROM (SELECT (@rownum := @rownum + 1) AS rn, ... ) t WHERE rn BETWEEN 11 AND 20 - 如果数据量大,这种写法会全表计算序号,务必确认业务能接受延迟
Oracle 11g R1 之前版本的伪列替代方案
老 Oracle 不支持 ROW_NUMBER(),但自带 ROWNUM 伪列——它只在结果集生成时分配,且不可回溯修改,所以不能直接用于排序后编号。
典型误用:SELECT ROWNUM, name FROM student ORDER BY score DESC,此时 ROWNUM 是按原始物理顺序分配的,跟 ORDER BY 无关。
- 必须用子查询包裹排序结果,再在外层选
ROWNUM:SELECT ROWNUM rn, name FROM (SELECT name FROM student ORDER BY score DESC) - 这种方式无法跳过前 N 行(如分页第 2 页),因为
ROWNUM总从 1 开始;要实现OFFSET,得用双重子查询或NOT IN排除前 N 行,性能极差 - 如果只要“前 N 名”,用
WHERE ROWNUM 是安全且高效的
变量序号本质是依赖执行顺序的副作用,不同数据库解析器行为差异很大。哪怕同一版本,开启查询缓存、使用视图或触发器都可能让变量失效。真要长期维护,优先考虑升级数据库或改用应用层编号。

















