limit offset, size 在大数据量下变慢是因为需扫描跳过 offset 行,导致大量随机i/o;优化方案包括覆盖索引、避免排序函数、子查询主键过滤及游标分页。

为什么 LIMIT offset, size 在大数据量下会越来越慢
因为 MySQL 实际执行时,必须先扫描并跳过 offset 行数据,哪怕这些行最终被丢弃。当 offset = 1000000 时,引擎仍要定位到第 1000001 条记录的物理位置——这涉及大量 B+ 树节点遍历和随机 I/O。偏移量越大,性能越接近全表扫描。
用覆盖索引减少回表,是提速最直接的一步
如果只需要分页展示 ID 和标题这类字段,别查 *,只查索引已包含的列:
-
SELECT id, title FROM article WHERE status = 1 ORDER BY id LIMIT 100000, 20比SELECT * ...快数倍甚至百倍,前提是(status, id)是联合索引或id是主键(聚簇索引) - 避免在
ORDER BY字段上使用函数或表达式,否则无法走索引;ORDER BY id DESC可用主键索引,但ORDER BY ABS(id)就不行 - 如果业务允许,把排序字段设为单调递增/递减(如
created_at),且该字段上有索引,后续可迁移到游标分页
用子查询 + 主键过滤替代大 offset
这是百万级以上表最常用、兼容性最好的优化方式,核心是把“跳过 N 行”变成“从某个确定值开始取”:
- 先查出起始 ID:
SELECT id FROM article ORDER BY id LIMIT 100000, 1 - 再查数据:
SELECT * FROM article WHERE id >= ? ORDER BY id LIMIT 20(? 是上一步查出的 ID) - 注意:必须确保
id严格递增且无重复(主键或唯一索引),否则可能漏数据;若排序依据不是主键,比如按updated_at分页,则需用(updated_at, id)联合索引,并在 WHERE 中写成WHERE (updated_at, id) > (?, ?)
游标分页(Cursor Pagination)适合高并发滚动场景
它不依赖页码,而是用上一页最后一条记录的排序字段值作为下一页起点,彻底规避 OFFSET:
- 第一页:
SELECT id, title, updated_at FROM article ORDER BY updated_at DESC, id DESC LIMIT 20 - 第二页(假设上一页最后一条是
updated_at = '2026-05-09 14:22:33',id = 88721):SELECT id, title, updated_at FROM article WHERE (updated_at, id) - 优点:任意深度响应稳定,毫秒级;缺点:不支持跳转到指定页码(如“跳到第 87 页”),且要求排序字段组合能唯一确定一行
真正卡住性能的往往不是 SQL 写法本身,而是没意识到 OFFSET 的代价藏在引擎层——它迫使 MySQL 做了大量无意义的定位工作。覆盖索引、主键过滤、游标三者不是互斥选项,而应按业务读写比例、是否需要随机跳页、数据更新频率来选型。比如后台管理类系统仍可用 id >= (subquery),而信息流 Feed 则必须上游标。











