结论是分页重复不是bug,而是mysql在order by字段值不唯一与5.6+优先队列(堆排序)优化共同导致的确定性行为;根本解法是在order by后追加唯一非空字段(如id)并保持排序方向一致,同时建立对应联合索引以确保索引生效。

直接说结论:分页重复不是 bug,是 MySQL 在 ORDER BY 字段值不唯一 + LIMIT 优化机制共同作用下的确定性行为。
MySQL 的优先队列排序导致相同值顺序不稳定
MySQL 5.6+ 对 ORDER BY ... LIMIT 查询做了优化,启用优先队列(Priority Queue)代替全量排序。它只维护 top-N 条记录,用的是堆排序 —— 而堆排序是不稳定的:当 create_time 相同的多行进入堆时,谁先被“挑中”取决于内存布局、执行路径甚至 buffer 状态,没有保证。
这意味着:
- 两次执行
SELECT * FROM t ORDER BY create_time DESC LIMIT 10, 10,即使数据没变,第二页开头那几条的相对顺序也可能不同 - 如果第一页末尾和第二页开头恰好有相同
create_time的记录,就可能“跳位”重复出现 - 这个现象在有索引但索引未覆盖全部排序字段时更明显(比如只有
create_time索引,没加id)
动态写入让 offset 分页彻底失效
用 LIMIT offset, size 的前提是“数据集静止”。一旦插入或删除发生,offset 就失准了:
- 第一页查完后插入一条
create_time更大的新记录 → 它挤到最前,原第 10 条变成第 11 条 → 第二页查询LIMIT 10, 10会把这条“新老大”和原第 10 条一起带上,造成重复 - 若第一页某条被删,所有后续行 offset -1 → 第二页漏掉一条(原第 11 条变成第 10 条,被 offset=10 跳过了)
- 哪怕加了唯一排序字段,只要用
offset,就逃不开这个逻辑缺陷
ORDER BY 多字段时索引是否生效很关键
加 id 是对的,但写法和索引决定它有没有用:
- 错误写法:
ORDER BY create_time DESC, id ASC—— 如果create_time是降序,id是升序,复合索引无法同时满足两个方向,MySQL 可能放弃走索引,退化为文件排序(Filesort),稳定性依旧差 - 正确写法:
ORDER BY create_time DESC, id DESC,并建联合索引(create_time, id)—— 这样能用上索引,排序完全由 B+ 树物理顺序保证,结果稳定 - 如果只按时间倒序查,又不想改索引,更稳妥的是游标分页:
WHERE create_time ,把上一页最后一条的 <code>create_time值传下来作为边界
真正容易被忽略的点是:问题常被归咎于“数据变了”,但即使数据完全静态,仅靠 ORDER BY non_unique_col LIMIT 就足以触发重复 —— 因为 MySQL 不承诺非唯一字段排序的跨查询一致性。稳定性的锚点必须是主键或其它唯一列,且必须参与索引设计和 SQL 写法的全程闭环。











