limit 1000000, 20会卡住,因mysql必须先排序完整结果集,再逐行扫描丢弃前1000000行,无法跳过;即使有索引,仍需遍历大量节点,i/o与cpu开销集中在无效扫描上。

直接用 LIMIT offset, size 查百万级数据的后几页,必然慢——MySQL 真的会扫描并丢弃前 offset 行,不是跳过。
为什么 LIMIT 1000000, 20 会卡住
MySQL 不支持“跳到第 N 行再取”,它执行时:先按 ORDER BY 排出完整结果集(哪怕只想要最后 20 条),再数够 1000020 行,扔掉前 1000000 行。这个过程无法利用索引跳过,全靠临时排序 + 逐行计数。
即使有 id 主键索引,ORDER BY created_at LIMIT 1000000,20 仍可能走全表扫描(除非 created_at 有覆盖索引);EXPLAIN 显示 rows 值极大,且 Extra 出现 Using filesort 或 Using temporary;偏移量每翻一倍,耗时几乎线性增长,不是对数级。
用覆盖索引 + 子查询做延迟关联
核心思路:先用子查询只查主键(轻量、走索引),再用主键回表拿完整字段。避免大字段和非索引列拖慢扫描。
原始慢查询:
SELECT * FROM orders WHERE status = 1 ORDER BY id DESC LIMIT 1000000, 20;
优化后:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY id DESC LIMIT 1000000, 20 ) AS tmp ON o.id = tmp.id;
- 子查询
SELECT id只读索引,极快;id必须是主键或有唯一索引,否则LIMIT在子查询中不被允许(MySQL 8.0.22+ 才支持非唯一索引子查询用LIMIT) - 外层
JOIN是等值匹配,能命中主键索引,回表成本可控 - 不能用
IN (subquery)替代——MySQL 不允许子查询里带LIMIT的IN - 复合条件必须加联合索引,比如
WHERE status = 1 ORDER BY id DESC,需建INDEX idx_status_id (status, id),否则子查询仍可能扫全表
改用游标分页(WHERE sort_col 形式)
适用于实时性要求不高、用户不跳页、只前后翻的场景(比如 feed 流)。它彻底抛弃 OFFSET,用上一页最后一条记录的排序字段值做条件。
例如按 updated_at DESC 分页,第一页:
SELECT * FROM orders WHERE status = 1 ORDER BY updated_at DESC, id DESC LIMIT 20;
拿到第 20 条的 updated_at 和 id(防时间重复),下一页:
SELECT * FROM orders WHERE status = 1 AND (updated_at
- 必须复合排序(如
updated_at, id),否则时间相同时结果不稳定 -
WHERE条件中的比较值必须来自上一页末尾记录,不能拼错方向(DESC对应,<code>ASC对应>) - 该方式在高并发写入场景下可能漏数据(新插入的记录落在游标范围内但未被查到),需结合业务容忍度判断
延迟关联和游标分页不是二选一,而是场景选择:要支持随机跳页(比如后台管理查第 87 页),只能用延迟关联;要保障接口响应稳定且接受顺序翻页(比如 App 列表下拉),游标分页更可靠。最容易被忽略的是索引设计——没建对联合索引,两种优化都白搭。











