limit 1000000,10慢是因为mysql必须顺序扫描并计数1000010行后丢弃前100万行,无法跳过;优化核心是绕开offset,首选游标分页(where id > last_id),次选延迟关联(子查询先取id再join)。

MySQL 8.0 中超大偏移量的 LIMIT 查询本质上无法靠“加索引”直接治好——它必须跳过前 OFFSET 行,而优化的核心是绕开这个跳过动作。
为什么 LIMIT 1000000, 10 会慢到 25 秒
MySQL 不是“定位到第 1000000 行再取 10 条”,而是:扫描排序索引,逐行读取并计数,直到累计读满 1000010 行,再丢弃前 1000000 行。整个过程是顺序 I/O + 内存搬运,OFFSET 越大,扫描行数和丢弃成本线性上升。
- 执行计划里看到
rows_examined接近OFFSET + LIMIT,就是这个行为的证据 - 即使
ORDER BY id字段有主键索引,也无法跳过中间行 -
WHERE条件如果没走索引,问题会更严重——先全表扫描再排序再跳偏移
Keyset 分页(游标分页)是首选方案
用上一页最后一条记录的主键值作为下一页起点,彻底消除 OFFSET。前提是排序字段单调、可比较、有索引(通常是主键或唯一递增字段)。
- 上一页查出最后一条的
id = 1049999,下一页就写:SELECT * FROM orders WHERE id > 1049999 ORDER BY id LIMIT 10 - 不能用
id >=,否则可能重复或漏掉边界值;也不能假设id连续,必须读取真实末尾值 - 前端需保存“游标值”,而不是页码;后端接口要接受
cursor参数而非page和size - 不支持跳转到任意页(比如直接翻到第 5000 页),但绝大多数真实场景(滚动加载、下拉刷新)不需要
MySQL 8.0.21+ 的 prefer_ordering_index 参数要慎用
这个参数控制优化器是否“优先选排序字段的索引”,默认开启(prefer_ordering_index=on)。在带 WHERE 条件的大分页中,它反而可能害你。
- 例如:
SELECT * FROM t WHERE status = 1 ORDER BY id LIMIT 100000, 10,若status有高选择性索引,但优化器因ORDER BY id强行走主键索引,就会全表扫 - 此时应关掉:
SET optimizer_switch = "prefer_ordering_index=off" - 但它只影响“排序索引 vs 过滤索引”的权衡,不解决
OFFSET本身的问题——关了也不会让LIMIT 1000000, 10变快,只是避免更差的执行计划
延迟关联(Deferred Join)适合多字段排序或非主键分页
当无法用主键做游标(比如按 create_time DESC 分页,且时间可能重复),又必须支持大偏移时,可用两步法减少回表数据量。
- 第一步只查排序字段和主键:
SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 10 - 第二步用这些
id回查完整行:SELECT * FROM orders WHERE id IN (1049999,1049998,...) - 要求
id是主键或有索引,否则第二步会变慢;也要求第一步能走覆盖索引(比如INDEX(create_time, id)) - 注意:
IN列表长度受max_allowed_packet限制,10 条安全,100 条需确认配置
真正难处理的是既要随机跳页、又要低延迟、还要数据实时——这种需求通常意味着设计阶段就该引入缓存层或预计算视图,而不是在 MySQL 单点硬扛。











