直接原因是ORDER BY字段不唯一导致分页结果不可靠,必须在ORDER BY后追加唯一字段(如主键id)作为二级排序,且方向需一致,否则会出现重复和丢失。

直接原因就是 ORDER BY 字段不唯一,导致 MySQL(或 PostgreSQL)每次执行时对相同值的行返回顺序不一致,LIMIT offset, size 切出来的“片”就不可靠——重复和丢失是必然结果,不是偶然 bug。
为什么 ORDER BY create_time 会出问题
时间字段(如 create_time、update_time)天然容易重复:同一秒插入多条记录很常见。MySQL 官方明确说明:如果多个行在 ORDER BY 列中有相同的值,服务器可以自由地以任何顺序返回这些行。这意味着:
- 第一次查
ORDER BY create_time DESC LIMIT 0,10,可能把 id=5、id=8、id=2 排在前几条; - 第二次查
ORDER BY create_time DESC LIMIT 10,10,相同时间戳的行被重新洗牌,id=5 又跑进了第二页; - 而原本该进第二页的某条记录,因为排序抖动被跳过了——这就是“丢失”。
必须补一个唯一字段做二级排序
核心解法不是换写法,而是让排序结果确定。最稳妥的做法是在 ORDER BY 后追加主键(或其他唯一索引字段),例如:
SELECT * FROM orders ORDER BY create_time DESC, id DESC LIMIT 10, 10;
这样做的关键点:
-
id是主键,绝对唯一,能打破所有create_time相同情况下的不确定性; - 顺序必须和分页方向一致:如果
create_time是DESC,id也建议DESC(避免跨页时因方向冲突引入新抖动); - 不要用
ORDER BY create_time DESC, id ASC—— 这会导致相同时间下 id 小的排前面,但下一页又从 id 大的开始切,反而更容易漏数据。
MySQL 5.6+ 的优先队列优化会让问题更隐蔽
MySQL 5.6 引入了 Priority Queue 优化 ORDER BY ... LIMIT,底层用堆排序(不稳定算法)。它只维护前 N 条记录,不全量排序——这放大了非唯一排序字段的风险:
- 即使你建了
create_time索引,只要该字段值重复,优化器仍可能放弃索引有序性,改用堆排序; - 你用
EXPLAIN看到Using filesort或Using priority queue,基本就能确认排序非稳定; - 这种优化本意是提速,但代价是牺牲排序确定性——不能依赖它“刚好没出错”。
别指望 DISTINCT 或 GROUP BY 治本
有人想用 DISTINCT 去重或 GROUP BY 聚合来绕开,这是无效的:
-
DISTINCT是对最终结果去重,不是控制分页切片逻辑,重复记录仍会出现在不同页里再被去重,业务层看到的仍是错乱; -
GROUP BY会改变结果集结构,丢失原始行信息,且无法保证每组取哪一条,完全偏离分页语义; - 窗口函数如
ROW_NUMBER() OVER (ORDER BY create_time)在 MySQL 8.0+ 可用,但注意:如果ORDER BY子句本身不唯一,ROW_NUMBER()分配的序号也会抖动——必须写成ROW_NUMBER() OVER (ORDER BY create_time DESC, id DESC)才真正可靠。
真正要命的是:这个问题在小数据量、低并发下几乎不暴露,一上生产、数据量破百万,或者用户快速翻页时才集中爆发。补二级排序字段是成本最低、兼容性最强、效果最确定的解法,别等线上报警才想起加个 , id。











