在MySQL中处理大表深分页时,直接使用limit offset size的写法会让存储引擎先读取offset加size行再做丢弃,带来大量无效IO。通过子查询先定位主键,再用延迟关联回表,可以只读取真正需要的行,从而降低IO开销。

传统大分页查询的问题
假设有一张订单表orders,数据量在千万级别,按创建时间倒序翻到第10000页,每页20条。常规写法如下:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 199980, 20;
该语句需要先排序并扫描约20万行,再返回最后20行,大多数扫描行在返回前就被丢弃,造成IO与CPU浪费。
利用子查询实现延迟关联
延迟关联的核心是先通过覆盖索引子查询拿到目标行的主键id,再与原表做join,只回表读取需要的记录:
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id
FROM orders
ORDER BY created_at DESC
LIMIT 199980, 20
) AS tmp ON o.id = tmp.id
ORDER BY o.created_at DESC;
子查询SELECT id FROM orders ORDER BY created_at DESC LIMIT 199980, 20只读取索引列,避免读取整行数据,外层join再按主键精确回表。
为什么能减少IO
- 子查询使用覆盖索引,不需要回表
- 仅对20个主键做回表,而非扫描20万行整行
- 排序和偏移在索引层完成,减少临时表与文件排序压力
执行计划对比
通过explain可以观察两种写法的差异:
| 查询方式 | type | rows | Extra |
|---|---|---|---|
| 传统limit | ALL | 199980+ | Using filesort |
| 延迟关联 | ref | 20 | Using index; Using join buffer |
使用注意事项
索引必须合理
延迟关联依赖排序字段与主键的索引,如果order by字段无索引,子查询仍会文件排序。建议建立created_at, id的联合索引。
子查询字段控制
子查询中只选主键或覆盖索引列,不要写SELECT *,否则失去延迟关联意义。
延迟关联不是万能方案,当offset极大且无法利用索引时,可考虑基于游标或时间戳的分页策略。
小结
在MySQL大分页场景中,通过子查询获取主键再做延迟关联,可以明显减少IO与回表数据量。实际落地时配合合适索引,往往能将深分页响应时间从秒级降至毫秒级。