MySQL 分页为什么会重复数据:补上 id 还不够

做列表接口时,很多人先写出这句:

sql 复制代码
SELECT id, title, created_at
FROM article
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;

数据少的时候没毛病。数据一多,用户偶尔会在第二页看见第一页的记录,或者有一条怎么翻都翻不到。

这事经常被一句"深分页慢"带过去,其实是两个不同的坑:排序不稳定,以及翻页期间数据发生了变化。只优化 OFFSET 的扫描量,修不好前一个;只在排序后补主键,也修不好后一个。

相同时间的记录,谁排前面没有保证

假设文章表里有三条数据的 created_at 都是 2026-08-08 10:00:00。SQL 只要求按时间倒序,并没有规定这三条内部怎么排。

MySQL 8.4 的官方手册写得很直白:如果多行的 ORDER BY 列值相同,服务端可以按任意顺序返回它们;执行计划变化时,这个顺序也可能变化。LIMIT 本身就可能影响执行计划。

所以这不是数据库"偶尔抽风"。查询写出来的顺序本来就不完整。

先把排序补成唯一的:

sql 复制代码
SELECT id, title, created_at
FROM article
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 20;

只要 id 唯一,任何两行都能分出先后。这里还有个小细节:两个字段必须保持同一套方向。时间倒序、同一时间下 id 也倒序,后面的游标条件才能原样对上。

这一步解决的是"同一份数据,多查几次顺序别变"。但线上列表不是一张静止的照片。

补了 id,为什么还会重复

第一页刚查完,顺序是:

text 复制代码
105, 104, 103, 102

页面大小是 2,客户端拿到了 105, 104

这时新文章 106 插到最前面。第二页继续执行 OFFSET 2,数据库看到的顺序已经成了:

text 复制代码
106, 105, 104, 103, 102

跳过前两行以后,第二页返回 104, 103104 就重复了。删除或更新排序字段时,同样可能漏行。

OFFSET 记录的是"跳过几行",不是"上一页停在哪里"。列表头部只要发生变化,这把尺子的零点就跟着动。

接口不要求跳页时,我会改成复合游标

第一页仍然这样查:

sql 复制代码
SELECT id, title, created_at
FROM article
ORDER BY created_at DESC, id DESC
LIMIT 20;

返回数据时,把最后一条的 created_atid 一起交给客户端。假设最后一条是:

text 复制代码
created_at = 2026-08-08 10:00:00
id         = 104

下一页从这个位置之后继续:

sql 复制代码
SELECT id, title, created_at
FROM article
WHERE created_at < '2026-08-08 10:00:00'
   OR (created_at = '2026-08-08 10:00:00' AND id < 104)
ORDER BY created_at DESC, id DESC
LIMIT 20;

这里不能只传时间。时间会重复,只用 created_at < ? 会直接漏掉同一秒里排在 104 后面的记录。游标、WHERE 条件、ORDER BY,三处都得是同一组字段。

MySQL 8.0 可以配对应的降序复合索引:

sql 复制代码
CREATE INDEX idx_article_created_id
ON article (created_at DESC, id DESC);

这样查询可以沿着索引从游标位置继续向后读,不必每一页都从头数到 OFFSET。实际有没有用到,还是看 EXPLAIN,别看见建了索引就默认优化器一定选它。

游标分页也不是"绝对不漏"

它解决的是最常见的滚动列表:用户往下翻时,头部不断有新数据进来。新行排在游标前面,不会把已经读过的行挤进下一页。

但如果翻页期间有人修改了旧记录的 created_at,它可能跨过游标;记录被删除后,也不可能继续返回。导出、对账这类要求严格一致的任务,应该固定快照或先固化待导出的主键集合,不能把一个在线列表接口硬当快照。

还有一个取舍。游标分页很适合"下一页"和无限滚动,不擅长直接跳到第 5000 页。产品真要求随机跳页,就得接受 OFFSET 的成本,或者另做页码到锚点的映射。

面试里被问到 MySQL 分页,我不会只答"深分页用游标"。先问排序键是否唯一,再问翻页期间有没有并发写入,最后才是扫描成本。三个问题对上的 SQL 不一样。

相关推荐
小张同学a.2 小时前
zabbix企业级监控平台3——服务监控与API实战
linux·运维·mysql·nginx·tomcat·zabbix
九皇叔叔2 小时前
RHEL9 安装 MySQL 8.4.11.LTS 版本
android·mysql·adb
海兰2 小时前
【数据库】tdsql(MySQL )的事务隔离级别
android·数据库·mysql
灵析表格2 小时前
Excel连接MySQL的函数化革命:灵析表格MySQL函数族业务应用分析
数据库·mysql·adb·excel·wps·灵析表格·excel公式盒子
ruleslol3 小时前
MySQL: Undo日志与MVCC多版本并发控制
mysql
二十雨辰3 小时前
[Java]-MySql面试题
mysql
延延oO3 小时前
Mysql --- InnoAL SQL 助手
数据库·mysql
ClouGence4 小时前
告别 Navicat!CloudDM:开源免费一站式数据库开发管理工具
数据库·sql·开源·数据库开发
Zhu7589 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes