limit offset, size查百万页后数据不可行,因mysql需扫描并丢弃前offset行,io和cpu开销线性增长;应改用游标分页、where+主键或延迟关联等方案。

直接用 LIMIT offset, size 查百万页以后的数据,在生产环境基本不可行——MySQL 必须扫描并丢弃前 offset 行,IO 和 CPU 开销随偏移量线性增长,不是“慢一点”,而是“查不动”。真正能落地的方案,得绕过 offset 这个设计缺陷本身。
为什么 LIMIT 1000000, 20 会扫 1000020 行?
MySQL 不支持基于行号的跳跃定位。即使 id 是主键、有 B+ 树索引,LIMIT 1000000, 20 的执行逻辑仍是:按 ORDER BY 顺序逐条读取,计数到 1000000 后才开始收集结果。这和“跳转到第 1000001 条”是两回事。
- 如果
ORDER BY字段没索引,还要额外触发filesort,性能雪上加霜 - 如果查询含
SELECT *且走非覆盖索引,每条记录都要回表,随机 IO 爆增 - 偏移量越大,越容易触发临时表或磁盘排序,内存不足时直接拖垮整个实例
基于主键/唯一索引的 WHERE + LIMIT 替代法
这是最常用、见效最快的改造方式,前提是排序字段和过滤条件能落在同一个有序索引上(通常是主键或带 id 的联合索引)。
- 第一页查完后,记住最后一条的
id值(比如last_id = 1234567) - 下一页改写为:
SELECT * FROM orders WHERE id > 1234567 ORDER BY id LIMIT 20 - 必须确保
WHERE条件和ORDER BY字段一致,否则可能漏数据或重复 - 不能用于跳页(比如从第 1 页直接跳到第 1000 页),只适合“下一页”连续翻页场景
延迟关联(Covering Index + JOIN)减少回表
当无法只靠主键排序(比如要按 created_at 分页,但 created_at 不是主键),又必须返回全字段时,用这个方案把“扫描范围”压回到几十行。
- 先用覆盖索引快速捞出需要的主键:
SELECT id FROM orders WHERE status = 'paid' ORDER BY created_at DESC LIMIT 1000000, 20 - 再用这些
id回原表查完整数据:SELECT o.* FROM orders o INNER JOIN (上面子查询) t ON o.id = t.id - 关键点:子查询里
ORDER BY created_at必须有对应索引(如(status, created_at)),否则子查询本身就会慢 - 比纯
LIMIT快一个数量级,但不如游标方案稳定,尤其在高并发更新场景下可能因数据变动导致分页错位
游标分页(Cursor-based Pagination)才是生产首选
用排序字段的值(而非行号)作为分页锚点,彻底摆脱 offset。它不保证“第 N 页”,但保证“下一页数据不重复、不遗漏、不跳变”,这才是 API 设计的真实需求。
- 第一页:
SELECT * FROM orders ORDER BY created_at DESC, id DESC LIMIT 20 - 拿到最后一条的
created_at和id(比如'2026-10-14 15:22:33', 9876543) - 下一页:
SELECT * FROM orders WHERE (created_at, id) - 复合条件必须和
ORDER BY完全一致,且字段顺序、升降序严格匹配,否则索引失效 - 前端需把游标值(base64 编码后的字符串)透传,不能解析或修改,否则分页断裂
最容易被忽略的是游标值的构造逻辑:如果排序字段有重复值(比如多个订单同秒创建),单靠 created_at 无法唯一确定位置,必须补上 id 或其他唯一字段组成复合游标,否则会漏数据或重复返回。











