LIMIT 1000000, 20 变慢是因为MySQL必须顺序扫描并计数前1000020行,即使有索引也无法跳过,OFFSET是逻辑偏移而非物理寻址,导致全表或聚簇索引扫描。

为什么 LIMIT 1000000, 20 会让查询变慢十倍?
不是因为跳过了100万行,而是MySQL必须先扫描并计数前1000020行才能返回最后20条——哪怕有索引,OFFSET 仍强制走聚簇索引的顺序扫描。InnoDB里没有“直接跳到第N页”的物理地址,OFFSET 是纯逻辑偏移。
常见错误现象:EXPLAIN 显示 rows 高达百万级,type 是 index 或 ALL,但实际业务只取20条;监控发现慢查询集中在分页靠后的请求。
- 使用场景:后台订单列表翻页、日志检索、数据导出分批拉取
- 性能影响:OFFSET 超过 10 万后,QPS 显著下降,CPU 和 I/O 压力陡增
- 兼容性注意:该问题在 MySQL 5.7 / 8.0 中均存在,不因版本升级自动缓解
用游标分页(cursor-based pagination)替代 LIMIT offset, size
核心思路是放弃“第几页”这个概念,改用“从某条记录之后取N条”。这要求排序字段严格唯一且有索引(推荐用主键或带唯一约束的 created_at + id 组合)。
例如原查询:
SELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 20
换成游标方式:
SELECT * FROM orders WHERE id < 5432109 ORDER BY id DESC LIMIT 20
- 必须确保
WHERE条件中的字段(如id)已建索引,且和ORDER BY字段一致 - 首次请求需先查出起始点(比如取第1页的最后一条
id),后续请求携带该值作为游标 - 不能跳页,但支持稳定、恒定耗时的“下一页”操作;对用户不可见的后台任务(如ETL)尤其适合
- 如果排序字段非唯一(如仅用
created_at),需补上主键消除歧义:WHERE (created_at, id) < ('2025-09-01', 888888)
当必须支持任意页码跳转时,怎么减少扫描量?
硬要保留 LIMIT offset, size 的接口,就只能把“全表扫描”变成“索引覆盖扫描”,再回表。关键在于让MySQL只遍历索引树,不触碰数据页。
示例优化写法:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id DESC LIMIT 1000000, 20) t ON o.id = t.id ORDER BY o.id DESC;
- 子查询
SELECT id ... LIMIT ...只走主键索引,体积小、速度快(InnoDB主键索引即数据本身,但这里只读键值) - 外层
JOIN利用主键等值查找,回表代价固定为20次随机IO,而非百万次顺序扫描 - 务必给排序字段建好索引(如
INDEX idx_created_id (created_at DESC, id DESC)),否则子查询仍慢 - 该方案在
OFFSET > 10万时效果明显,但不如游标分页彻底;慎用于高并发随机跳页场景
别忽略 SQL_NO_CACHE 和查询缓存干扰
MySQL 8.0 已移除查询缓存,但 5.7 及更早版本中,若开启 query_cache_type=1,深度分页查询可能因缓存失效频繁导致锁争用,反而拖慢整体性能。
- 检查当前设置:
SHOW VARIABLES LIKE 'query_cache_type'; - 临时绕过缓存(仅调试用):
SELECT SQL_NO_CACHE * FROM ... - 真正有效的做法是关闭查询缓存(
SET GLOBAL query_cache_type = 0;),尤其当业务含大量动态分页时 - 注意:关闭后需重启连接才对已有会话生效;应用层若复用长连接,应主动重连或设会话级变量
游标分页看似多了一步状态管理,但它把性能不确定性换成了可预测的常数级延迟。而试图“修好 LIMIT”往往只是把问题往后推——OFFSET 越大,越暴露设计瓶颈。真正难的不是写对SQL,是让前端接受“不能输页码跳到第5000页”这个事实。


















