row_number()是唯一可靠、连续不跳号的序号生成方式,必须配合over(order by ...)使用,编号严格按排序顺序逐行赋值1,2,3…,不处理重复值;误用rank()或dense_rank()会导致跳号,并非真正连续。

ROW_NUMBER() 是最直接的连续序号生成方式
需要连续、不跳号、按指定顺序编号时,ROW_NUMBER() 是唯一可靠选择。它不关心值是否重复,只按 ORDER BY 子句严格排序后逐行赋值 1, 2, 3…
常见错误是误用 RANK() 或 DENSE_RANK():它们遇到相同排序值会并列,导致序号跳变(比如两个并列第1名后直接到第3名),根本不符合“连续”要求。
实操要点:
-
ORDER BY必须明确——哪怕只是按主键ORDER BY id,空ORDER BY在多数数据库中不合法 - 如果想按业务字段排序但该字段有 NULL,注意不同数据库对 NULL 的默认排序位置(PostgreSQL 默认排最后,MySQL 可能排最前),建议显式写成
ORDER BY status NULLS LAST(PostgreSQL)或ORDER BY IFNULL(status, 'z')(MySQL) - 别在子查询里漏写别名:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) AS rn FROM orders——rn这个别名后续过滤或分页会用到
WHERE 和 ORDER BY 的顺序影响结果正确性
窗口函数在 SQL 执行逻辑中晚于 WHERE 但早于 ORDER BY(最终结果排序)。这意味着:ROW_NUMBER() 编号的是过滤后的数据集,不是全表。
典型陷阱:想取“最新 10 条记录的序号”,却写成:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn FROM logs ORDER BY created_at DESC LIMIT 10;
这实际是对全表编号后再取前 10 行,序号可能是 1–10,也可能是 991–1000 —— 完全不可控。
正确做法是先过滤再编号:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn FROM ( SELECT * FROM logs ORDER BY created_at DESC LIMIT 10 ) t;
或者更稳妥地用 CTE:
WITH top10 AS ( SELECT * FROM logs ORDER BY created_at DESC LIMIT 10 ) SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn FROM top10;
分区(PARTITION BY)会重置计数,慎用
加 PARTITION BY 后,ROW_NUMBER() 在每个分组内独立计数,不再是全局连续。例如按用户分组编号:ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at),结果是每个用户自己的 1, 2, 3…
如果你本意是全局连续,但不小心写了 PARTITION BY,就会发现序号频繁归零——检查执行计划或抽样几条数据就能快速定位。
需要“分组内连续 + 全局唯一”时,不能靠单个窗口函数解决。常见替代方案:
- 先用
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)得到组内序号 - 再用
ROW_NUMBER() OVER (ORDER BY user_id, created_at)得到全局序号 - 或者用
DENSE_RANK() OVER (ORDER BY user_id, created_at)(注意去重逻辑差异)
不同数据库对 NULL 和类型隐式转换的处理差异
同一个 ROW_NUMBER() OVER (ORDER BY x) 在 PostgreSQL、MySQL、SQL Server 上可能给出不同序号,尤其当 x 列含 NULL 或混合类型(如字符串和数字混存)时。
比如 MySQL 8.0 中,ORDER BY status 若 status 是 ENUM 类型,排序依据是定义顺序而非字典序;而 PostgreSQL 的 ENUM 排序才是按声明顺序,但若转成 TEXT 再排,结果就变了。
规避方法很实在:
- 始终把排序字段显式转为统一类型:
ORDER BY CAST(price AS DECIMAL(10,2)) - 用
CASE WHEN显式控制 NULL 位置:ORDER BY CASE WHEN updated_at IS NULL THEN 1 ELSE 0 END, updated_at DESC - 上线前在目标库跑一次
EXPLAIN看实际排序键,比文档更准
连续序号看着简单,但一旦涉及分页、过滤、多库兼容,很容易在边界 case 上出错——尤其是 NULL、类型模糊、分区逻辑叠加的时候,别依赖“应该没问题”的直觉。











