LIMIT 100000,20慢是因为MySQL必须逐行扫描并丢弃前100000行,即使只取20条;延迟关联通过子查询先用覆盖索引获取ID、外层再精准回表,将回表次数从100020次降至20次。

大偏移量 LIMIT offset, size 查询慢,不是因为数据多,而是 MySQL 必须逐行扫描并丢弃前 offset 行——哪怕只取 20 条,也要读 100020 行。延迟关联能把它压到只读 20 行索引 + 20 次主键回表。
为什么 LIMIT 100000, 20 会扫几十万行?
MySQL 不支持“跳转到第 N 行”,它执行时会:从排序字段索引起点开始,逐行读、逐行计数,直到累计读够 100000 + 20 行,再扔掉前 100000 行。如果 SELECT * 且没覆盖索引,每行都触发一次回表(随机 IO),I/O 成本线性暴涨。
-
EXPLAIN中rows显示几十万,Extra出现Using filesort或Using temporary,基本就是这个病 - 即使
id有主键索引,ORDER BY create_time LIMIT 100000, 20仍可能慢——因为排序字段和主键不一致,无法用主键索引直接跳 - 加了单列
INDEX(create_time)不够,必须是包含排序字段 + 主键的联合索引,例如INDEX(create_time, id)
延迟关联怎么写?关键三步不能错
把“找 ID”和“查数据”拆开,子查询走覆盖索引捞主键,外层用主键精准回查。
- 子查询只选主键(如
id),且ORDER BY字段必须与索引最左前缀严格匹配:例如索引是INDEX(status, create_time, id),子查询就得写ORDER BY status, create_time, id - 外层
JOIN必须用主键或唯一索引字段等值连接:ON t.id = tmp.id;若用name这类非唯一字段,JOIN可能退化为全表扫描 - 必须补全排序稳定性:当
create_time可能重复时,ORDER BY create_time DESC, id DESC才能保证分页不漏、不重
正确示例:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 'shipped' ORDER BY create_time DESC, id DESC LIMIT 100000, 20 ) AS tmp ON o.id = tmp.id;
哪些情况延迟关联会失效?
它不是自动生效的银弹,常见翻车点集中在索引和语义上。
- 子查询里用了函数,比如
ORDER BY DATE(create_time)—— 索引失效,直接退化为文件排序 -
ORDER BY字段含大量NULL,而索引未声明NOT NULL或未在WHERE中排除,可能导致排序结果错乱 - 业务要求返回总条数(
COUNT(*)),延迟关联本身不解决这个问题;SQL_CALC_FOUND_ROWS在 MySQL 8.0+ 已废弃,得单独查或缓存估算 - 排序字段非单调、有空洞(比如删过大量中间记录),
WHERE id > ?类游标法更稳,延迟关联只适合保留跳页能力的场景
延迟关联 vs 游标分页:什么时候该换思路?
延迟关联性能随 offset 缓慢下降,适合后台管理页码输入;游标分页性能恒定,但必须记住上一页末尾值。
- 用户端瀑布流、消息列表这类“只下一页”的场景,直接用游标:
WHERE (create_time, id) ,配合 <code>INDEX(create_time, id) - 混合需求(前 100 页可跳,之后强制滚动)建议在应用层路由:小
offset用延迟关联,超过阈值(如 10000)自动切到游标逻辑 - 延迟关联的子查询若没走索引,比原查询还慢——务必用
EXPLAIN验证子查询的type是index或range,且key显示命中目标索引
真正容易被忽略的是排序字段的稳定性:时间戳重复、ID 不连续、索引顺序与 ORDER BY 方向不一致,都会让子查询返回的 ID 序列漂移,最终导致分页错乱——这不是性能问题,是数据一致性风险。

















