limit offset在大数据量分页中必然变慢,因mysql需扫描并丢弃前offset行;推荐用延迟关联(子查询预取id)或游标分页替代。

别用 LIMIT 和 OFFSET 做大数据量分页——它不是慢,是设计上就注定会崩。 当 offset 超过 10 万,查询耗时从毫秒级跳到秒级甚至卡住,这不是索引没建好,而是 MySQL 必须扫描、排序、丢弃前 offset 行,再取后面几条。你越往后翻页,它越努力做无用功。
为什么 LIMIT OFFSET 在千万级表里必然变慢
MySQL 不会“跳”到第 N 行,而是按索引顺序逐行读取,直到凑够 OFFSET + LIMIT 行。比如 LIMIT 1000000, 20,它得读 1000020 行,再扔掉前 1000000 行:
- 如果
ORDER BY字段没索引,触发filesort,内存不够就写磁盘临时文件 - 如果
SELECT *且排序字段不在覆盖索引里,每读一行都可能触发一次回表(主键查找),IO 次数 = 扫描行数 - offset 越大,B+ 树遍历路径越深,随机 IO 越多,CPU 和 I/O 开销线性增长
替代方案:用子查询预取 ID 实现延迟关联
这是改动最小、收益最大的优化,适用于仍需支持“跳转任意页码”的场景(比如后台管理页):
- 确保 WHERE 条件字段 + ORDER BY 字段有联合索引,例如
INDEX(status, create_time) - 子查询只查主键
id,走覆盖索引,不回表 - 外层用这 20 个
id精准 JOIN 原表,把回表次数从 ~1000020 次压到 20 次
示例语句:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT id FROM orders WHERE status = 'PAID' ORDER BY create_time DESC LIMIT 1000000, 20 ) t2 ON t1.id = t2.id;
更优解:游标分页(Seek Pagination)
如果你的业务是“下拉加载更多”或 API 分页接口(如 APP 列表),直接放弃页码,改用游标:
- 第一页查出最后一条的
create_time和id(比如'2024-06-05 14:22:18',9876543) - 第二页用
WHERE create_time ,再 <code>ORDER BY create_time DESC, id DESC LIMIT 20 - 必须补主键去重:时间字段可能重复,单靠
created_at无法保证顺序唯一 - 不能跳页,但性能恒定,无论第几页都是毫秒级
容易被忽略的关键点
游标分页看似简单,但实际落地常栽在细节上:
-
ORDER BY字段必须有有效索引,且是联合索引的最左前缀;WHERE条件字段也要包含在该索引中 - 前端必须保存上一页末尾记录的完整排序字段值,不能只传
id或只传时间 - 如果业务允许数据实时插入且要求严格顺序,需注意新插入记录可能“插队”,游标边界要加防重逻辑
- 延迟关联虽快,但子查询的
LIMIT offset, size本身仍有性能拐点,建议 offset 控制在 10 万以内,超了就切游标











