row_number()不会生成真正重复编号,问题根源是order by字段重复导致排序不稳定,必须在排序列后追加唯一列(如id)确保每行位置唯一,否则分页、去重、导出均可能错乱。

ROW_NUMBER() 本身不保证唯一性,除非排序条件能完全区分每一行——这是很多人踩坑的根源。
为什么 ROW_NUMBER() 会生成重复编号?
当 ORDER BY 子句中存在相同值(比如多个用户 created_at 完全一致),ROW_NUMBER() 会按任意顺序分配序号,但结果不可预测且可能“看起来重复”(实则是排序不稳定导致的显示错觉)。真正的问题不是函数出错,而是排序键未提供足够区分度。
- 数据库不会自动加主键或隐式唯一字段到排序逻辑里
- 即使表有
id,不显式写进ORDER BY就不算参与排序 - 某些 ORM 或中间件可能截断时间精度(如把
2024-01-01 10:00:00.123存成2024-01-01 10:00:00),加剧重复
如何构造唯一排序条件?
核心做法:在业务排序字段后,追加一个能保证全局唯一的列(通常是主键),用逗号拼接进 ORDER BY。
SELECT id, name, created_at, ROW_NUMBER() OVER (ORDER BY created_at DESC, id ASC) AS rn FROM users;
- 优先按业务关心的字段排序(如
created_at DESC) - 再用主键
id(或其它非空唯一列)做“决胜字段”,确保每行排序位置唯一 - 避免用
ROWID或ctid(PostgreSQL)等物理标识——它们不跨事务稳定,且部分数据库不支持在窗口函数中使用 - 如果主键是复合键,需全部列出:
ORDER BY status, updated_at DESC, tenant_id, record_id
遇到 NULL 值怎么办?
NULL 在排序中默认排最前(ASC)或最后(DESC),但不同数据库行为不一致,且多个 NULL 之间无法区分。必须显式控制。
- 用
COALESCE(created_at, '1970-01-01')统一补值(注意类型匹配) - 更安全的做法是用
CASE WHEN把NULL排到末尾:ORDER BY (CASE WHEN created_at IS NULL THEN 1 ELSE 0 END), created_at DESC, id - 千万别依赖
ORDER BY created_at DESC NULLS LAST—— SQLite 不支持,MySQL 8.0.22+ 才支持,兼容性差
性能与索引建议
窗口函数本身不走索引,但排序字段如果有对应索引,能显著加快执行速度。尤其当数据量大、且只取前 N 行时,优化空间明显。
- 为常用排序组合建联合索引,例如:
CREATE INDEX idx_users_created_id ON users(created_at DESC, id ASC) - 如果经常按
status分组编号,索引应包含分区字段:CREATE INDEX idx_users_status_created_id ON users(status, created_at DESC, id) - 避免在
ORDER BY中使用函数(如DATE(created_at)),会导致索引失效
真正决定 ROW_NUMBER() 是否“唯一”的,从来不是函数本身,而是你给它的排序契约是否足够严密——漏掉一个 id,就可能让分页错位、去重失效、导出乱序。











