MySQL 8.0+ 应使用 ROW_NUMBER() 窗口函数,需配合 OVER(ORDER BY ...),不可用于 WHERE 或 GROUP BY,且必须嵌套子查询才能过滤行号。

MySQL 8.0+ 直接用 ROW_NUMBER() 最省事
如果你用的是 MySQL 8.0 或更高版本,ROW_NUMBER() 是最直观、最可靠的选择。它本质是窗口函数,必须配合 OVER() 使用,且不能放在 WHERE 或 GROUP BY 后面——这些地方还没到行号生成阶段。
常见错误是写成 SELECT *, ROW_NUMBER() FROM table,这会报错:ERROR 3593 (HY000): Window function 'row_number' is missing required ORDER BY clause。窗口函数必须明确排序依据,否则行号无意义。
实操建议:
-
OVER(ORDER BY id)按主键排,保证稳定;如果只是想要“原始顺序”,而表又没自增主键,得先加个临时排序字段(比如用UUID()或@row := @row + 1,但后者不推荐) - 别在
WHERE里引用ROW_NUMBER()别名(如WHERE rn > 10),必须套一层子查询或 CTE - 如果只想要前 20 行带编号,写成
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM t) tmp WHERE rn
MySQL 5.7 或更老版本用变量模拟
老版本不支持窗口函数,只能靠用户变量 @row 自增。但要注意:变量行为在 MySQL 中**不保证执行顺序**,尤其当查询涉及 JOIN、ORDER BY 或优化器重排时,行号可能错乱甚至重复。
安全用法只有一种:确保查询结果本身已按确定顺序排好,且不带任何可能触发并行扫描的结构(比如没索引的 ORDER BY)。
实操建议:
- 初始化变量必须和 SELECT 写在同一语句里:
SELECT @row := @row + 1 AS rn, t.* FROM t, (SELECT @row := 0) r ORDER BY t.id - 千万别把变量初始化写在前面单独的
SET @row = 0,然后跟一个 SELECT——在某些客户端或连接池下,变量作用域可能失效 - 如果用了
LIMIT,必须把变量赋值和ORDER BY都放在子查询里,否则 LIMIT 可能先截断再编号
PostgreSQL 用 ROW_NUMBER() 更宽松
PostgreSQL 的 ROW_NUMBER() 兼容性更好,允许在 SELECT 里直接使用,也支持在 CTE 或视图中嵌套。但它同样要求 OVER() 里有 ORDER BY,否则报错 window function row_number requires an ordering clause。
一个容易被忽略的点是:如果 ORDER BY 字段有重复值,ROW_NUMBER() 仍会强制给不同行分配不同序号(不像 RANK() 那样并列),这点适合做唯一行号,但不适合做“排名”。
实操建议:
- 想跳过前 10 行取接下来 10 行?直接
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) AS rn FROM logs) t WHERE rn BETWEEN 11 AND 20 - 如果排序字段为空值多,记得加
NULLS LAST或NULLS FIRST显式控制位置,否则不同 PostgreSQL 版本默认行为可能不同
SQL Server 和 Oracle 也支持标准窗口函数
SQL Server 2005+、Oracle 12c+ 都原生支持 ROW_NUMBER(),语法和 PostgreSQL 基本一致。但 SQL Server 有个坑:ORDER BY 在窗口函数里和最终查询的 ORDER BY 是两回事,后者不影响行号生成顺序。
比如你写 SELECT *, ROW_NUMBER() OVER (ORDER BY name) AS rn FROM t ORDER BY id,行号还是按 name 排的,不是按 id。
实操建议:
- Oracle 中如果表很大,
ROW_NUMBER()会强制全表扫描再排序,比用ROWNUM(伪列)慢得多;但ROWNUM不支持开窗,无法真正“按某字段排序后编号”,只能按物理读取顺序编号 - SQL Server 如果只需要分页,优先用
OFFSET-FETCH,比套一层ROW_NUMBER()性能更好,尤其数据量大时
实际用哪一种,取决于你的数据库版本和是否真需要“按业务逻辑排序后的稳定行号”。变量方案看着简单,但线上环境出过太多诡异问题;窗口函数是正解,只是老版本绕不开兼容性妥协。

















