根本解法是order by后追加唯一非空字段(如id)确保排序确定性;因mysql允许重复排序值行序不确定,尤其5.6+堆排序优化导致分页重复或丢失。

不加确定性排序的分页,本质是在赌数据库返回顺序——而它一定会输。
ORDER BY 字段有重复值时,MySQL 不保证行序稳定
只要 ORDER BY 涉及的列存在重复值(比如多个订单的 created_at 完全相同),MySQL 就有权以任意物理顺序返回这些行。这不是 bug,是标准允许的未定义行为。
- 现象:第 1 页出现
id=5,第 2 页又出现id=5;或刷新后某条记录“消失” - 原因:
ORDER BY created_at DESC只约束了时间维度,没约束同时间下的记录谁先谁后 - 优化器可能因执行计划变化(如索引选择、并行扫描)改变内部迭代顺序,导致两次查询结果错位
OFFSET/LIMIT 依赖排序稳定性,但本身不提供稳定性
LIMIT 20 OFFSET 40 的语义是“跳过前 40 行,取接下来 20 行”,但它跳过的那 40 行,必须每次都是同一组——而这完全取决于 ORDER BY 是否能每次都产出相同序列。
- 错误写法:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 40 - 问题根源:当第 41 行和第 40 行
created_at相同时,数据库可能把原本在第 40 行的记录移到第 41 行,导致它被跳过或重复 - 索引无法解决这个问题:即使
created_at有索引,重复值区间内仍无确定顺序
ROW_NUMBER() 和游标分页同样逃不开这个前提
窗口函数和键集分页只是换了一种分页机制,但底层都靠 ORDER BY 定序。如果排序不稳,它们只会把不稳定“固化”得更隐蔽。
-
ROW_NUMBER() OVER (ORDER BY created_at DESC):重复时间戳下,行号分配可能每次不同 - 游标分页
WHERE (created_at, id) :若漏掉 <code>id,只比created_at,就会漏数据或重复 - 正确兜底方式:所有排序字段组合必须能唯一标识一行,常见做法是末尾加上主键,如
ORDER BY created_at DESC, id DESC
最容易被忽略的一点:确定性排序不是“加了 ORDER BY 就万事大吉”,而是要求整个排序表达式在逻辑上无歧义——哪怕并发插入、索引重建、统计信息更新,结果顺序也不能漂移。这几乎必然意味着,至少有一个参与排序的字段是非空且唯一的。











