LIMIT 1000000, 10 慢是因为MySQL必须扫描1000010行并丢弃前1000000行,无论有无索引;优化方案包括延迟关联(先查ID再回表)和游标分页(基于排序字段锚点过滤)。

直接说结论:LIMIT 1000000, 10 慢,不是因为数据多,而是 MySQL 必须从头扫描 1000010 行、丢掉前 1000000 行——无论有没有索引,只要用 LIMIT offset, size,这个“数数再扔”的过程就绕不开。
为什么 LIMIT 1000000, 10 会扫上百万行
MySQL 不支持“跳转到第 N 行”,它执行分页时实际走的是:全量排序 → 逐行计数 → 数到 1000010 → 扔掉前 1000000 → 返回后 10 行。即使你有 id 主键索引,SELECT * 也会触发大量回表;如果 ORDER BY 字段没索引,还会额外触发 filesort。
常见错误现象:
-
EXPLAIN显示rows接近offset + size(比如 1000010),而不是预期的 10 - 加了
WHERE status = 1但没配对应联合索引,导致先全表扫描再过滤 - 用
created_at排序却只对id建了主键索引,ORDER BY created_at无法走索引
延迟关联:兼容跳页且改动最小的优化写法
核心是把“查全字段”拆成两步:先用覆盖索引极速定位 ID,再用这 10 个 ID 精准回表。不改业务逻辑,前端仍可输入任意页码。
实操建议:
- 必须建联合索引,例如
INDEX idx_created_at_id (created_at, id)(按ORDER BY字段在前) - 子查询只选
id,确保走覆盖索引;外层用JOIN关联原表取完整数据 - 避免在子查询里写
SELECT *或多余字段,否则覆盖索引失效
示例(按时间倒序查订单):
SELECT t.* FROM orders t JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC, id DESC LIMIT 1000000, 10 ) tmp ON t.id = tmp.id;
游标分页:性能最稳但需产品配合
适用于“下一页”类场景(如 Feed 流、后台列表滚动加载),彻底抛弃 OFFSET,靠上一页末尾记录的排序值做条件过滤。
使用前提与坑点:
- 排序字段必须有唯一性保障,否则可能漏/重数据;常用组合是
(created_at, id)或(updated_at, id) - 不能直接跳到第 1000 页;如果前端强行传
page=1000,后端得拒绝或降级为延迟关联 - 第一次请求需明确起始点,例如
WHERE created_at < '2026-09-01',而非空条件
示例(第二页请求,以上一页最后一条 created_at='2026-08-30 14:22:05', id=999999 为锚点):
SELECT * FROM orders WHERE (created_at < '2026-08-30 14:22:05') OR (created_at = '2026-08-30 14:22:05' AND id < 999999) AND status = 1 ORDER BY created_at DESC, id DESC LIMIT 10;
别忽略业务层限制和索引细节
再好的 SQL 也扛不住无约束的页码输入。很多慢查询其实源于没人检查过“用户是否真需要看第 10 万页”。
容易被忽略的关键点:
- 前端传来的
page和size必须做校验,例如限制page * size <= 100000,超限直接返回空或提示 -
WHERE条件字段没进联合索引,会导致先全表扫描再排序,延迟关联也救不了 - 时间字段用
DATETIME但查询带时区转换(如CONVERT_TZ),索引会失效 - 分布式 ID 或逻辑删除导致
id不连续,BETWEEN或简单id > last_id可能漏数据
真正卡住线上接口的,往往不是第 1000 万条数据本身,而是没人在意那条 LIMIT 1000000, 10 是怎么生成的——它可能来自一个没加校验的管理后台搜索框,也可能来自某次导出脚本的硬编码页码。


















