延迟关联能绕过深分页扫描黑洞,因其先通过索引仅查id缩小结果集,再精准回表,将io和cpu消耗从线性降为常数级;需满足三条件:子查询只查id、order by字段有匹配联合索引、外层用inner join而非in。

延迟关联为什么能绕过深分页的扫描黑洞
因为MySQL执行LIMIT offset, size时,并不会跳过前offset行,而是老老实实扫描并丢弃——哪怕你只想要20条,它也可能扫100万行、做100万次回表。延迟关联的核心思路是“先缩圈,再取数”:内层子查询只走索引查id(或主键),不碰大字段;外层用这些id精准回表。这样就把IO和CPU消耗从“线性增长”压回到“常数级”。
怎么写才真正生效:三个硬性条件缺一不可
很多人照着示例改了SQL,EXPLAIN一看还是type: ALL或Using filesort,问题往往出在没守住这三条底线:
- 子查询必须只查
id(或唯一主键),不能带*、不能查created_at等非主键字段,否则失去覆盖索引优势 -
ORDER BY字段必须有索引,且该索引要能同时支撑WHERE条件和排序——比如WHERE status = 1 ORDER BY created_at DESC,就得建联合索引(status, created_at),而不是单列索引 - 外层
JOIN必须用INNER JOIN ... ON t.id = tmp.id(或USING(id)),不能写成WHERE t.id IN (SELECT id ...),后者会让优化器放弃物化子查询,退化为嵌套循环
存储过程中用延迟关联的致命陷阱
在CREATE PROCEDURE里直接拼LIMIT p_offset, p_size是高危操作:每次调用都会重解析SQL,优化器面对大p_offset容易误判,索引突然失效,性能波动剧烈。正确做法是把延迟关联逻辑固化进过程体,且注意:
- 参数类型必须和字段类型严格一致,比如
status是TINYINT,就别传字符串'1',否则触发隐式转换,idx_status_created直接作废 - 禁止用
CONCAT + EXECUTE拼SQL,否则慢查询日志里看不到真实语句,DBA无法定位,执行计划也无法复用 - 子查询里的
ORDER BY字段顺序必须和联合索引定义顺序完全一致,建的是(status, created_at),就不能写成ORDER BY created_at, status
EXPLAIN里最关键的三个信号
改完SQL后别急着压测,先看EXPLAIN输出是否满足这三个特征,否则优化大概率没落地:
- 内层子查询的
type是range或index,不是ALL——说明WHERE条件走了索引 - 内层
Extra列没有Using filesort——说明排序直接利用了索引有序性 - 外层
type是eq_ref(用主键JOIN)或ref(用唯一索引JOIN),不是ALL或index——说明回表是精准的,没发生全表扫描
真正容易被忽略的是:即使SQL结构对了,只要WHERE条件里用了函数(比如DATE(created_at) = '2026-01-01')、或者联合索引字段顺序和查询顺序错位,整个延迟关联就形同虚设——它不报错,但性能和原始LIMIT一样烂。











