根本原因是order by排序键不唯一,导致分页时基准漂移;必须在排序字段后追加唯一字段(如id)确保顺序稳定,或改用row_number()窗口函数固化行号。

分页出现重复,根本不是分页语法写错了,而是 ORDER BY 的排序键不唯一,导致数据库每次取“第 N 行”时基准漂移——哪怕没并发修改,也会重复。
ORDER BY 字段重复时,LIMIT 分页必然不稳定
MySQL、PostgreSQL、Oracle 都一样:当 ORDER BY created_at 中多个记录的 created_at 值相等,数据库无法保证它们在结果集中的相对顺序固定。翻页时,前一页末尾和后一页开头可能恰好是这批“同时间”的记录,部分被漏掉、部分被重读。
- 现象:第 1 页返回了
id=101,102,103(created_at='2026-07-20 10:00:00'),第 2 页又出现id=102 - 原因:
created_at不是主键,也没加索引;数据库内部排序时,这批时间相同的行物理位置可能变动 - 验证方法:执行
SELECT created_at, COUNT(*) FROM table GROUP BY created_at ORDER BY COUNT(*) DESC LIMIT 5,看是否有大量重复值
必须补一个唯一字段到 ORDER BY 末尾
这不是“锦上添花”,而是稳定分页的硬性要求。唯一字段优先选主键(id),次选 rowid(Oracle)、ctid(PostgreSQL)或带唯一约束的业务字段。
- 正确写法(MySQL/PostgreSQL):
ORDER BY created_at DESC, id DESC - Oracle 推荐:
ORDER BY created_at DESC, rowid(rowid天然唯一且索引友好) - 错误写法:
ORDER BY created_at DESC, updated_at DESC——如果两个字段都可能重复,仍不稳 - 注意:多字段
ORDER BY会降低索引命中率;建议建联合索引(created_at, id)覆盖排序
用窗口函数替代 OFFSET 是更可靠的方案
当数据量大、OFFSET 值高(比如 LIMIT 10000, 20),数据库要先跳过 10000 行再取数,性能差且易因并发更新引入重复。窗口函数把行号固化在结果里,规避了“跳行”逻辑。
- MySQL 8.0+ / PostgreSQL / SQL Server 示例:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn FROM orders ) t WHERE t.rn BETWEEN 101 AND 120;
- 优势:每行的
rn在子查询中已确定,后续过滤不会受排序波动影响 - 注意:不能写成
WHERE rn > 100 LIMIT 20,因为优化器可能提前截断,导致漏数据;必须用BETWEEN或明确范围 - 旧版本 MySQL 5.7 只能靠应用层维护上次分页的
(created_at, id)值,用WHERE (created_at, id) 实现游标分页
最容易被忽略的一点:分页重复问题,90% 源于你把 ORDER BY 当成了“美化显示”的可选项,而没意识到它是分页语义的锚点。只要排序键不唯一,无论用什么数据库、什么分页方式,都只是在赌运气。











