mysql深分页慢因必须扫描并丢弃前offset行;优化需绕过offset计数或减轻扫描负担,如子查询只选id配合覆盖索引,或游标分页用where id > ?替代limit offset。

直接用 LIMIT offset, size 做深分页,MySQL 就一定会全表(或全索引)扫描前 offset + size 行——这不是配置问题,是执行引擎的固有行为。优化方向只有两个:绕过 offset 计数,或让扫描本身变轻。
为什么子查询只选 id 能大幅降低扫描成本
MySQL 执行 LIMIT 1000000, 20 时,必须从头开始逐行计数,直到第 1000020 行。如果子查询只查 id,且 ORDER BY 字段有合适索引,就能全程在索引树上完成扫描,不回表、不加载大字段、不触发临时表。
- 索引必须覆盖排序和过滤:比如
WHERE update_time > '2025-01-01' ORDER BY id,就要建(update_time, id)联合索引 - 避免在子查询里用
SELECT *或带大字段的列,否则索引失效,直接退化为全表扫描 - MySQL 8.0+ 支持降序索引,
ORDER BY created_at DESC应显式建(created_at DESC),否则 5.7 只能反向遍历,性能打折
延迟关联写法中 JOIN 比 IN 更可靠
外层用 IN (SELECT id ... LIMIT ...) 在 MySQL 8.0 以前不支持子查询带 LIMIT,即使支持,优化器也可能将 IN 重写为物化临时表,失去索引优势。
- 必须写成
INNER JOIN (SELECT id ... LIMIT ...)形式,确保子查询结果集被当作驱动表 - 外层
ON t.id = tmp.id必须用主键或唯一索引字段,否则JOIN会放大扫描量 - 如果业务允许,把
WHERE条件下推到子查询里,比如WHERE status = 1写在子查询内,而不是外层
游标分页为什么比任何 LIMIT 都快
游标分页不是“跳过 N 行”,而是“从某条记录之后取 N 行”。MySQL 可以直接在 B+ 树叶子节点链表上往后走,完全规避计数逻辑。
- 适用场景明确:APP 下拉加载、后台列表翻页等只支持“下一页”的业务
- 复合排序需拆解条件:比如
ORDER BY updated_at DESC, id DESC,下一页 WHERE 条件就得写成(updated_at, id) 或手动展开为 OR 逻辑 - 时间字段重复率高时,必须把主键加进排序和索引,否则同时间戳的多行顺序不确定,游标会漏数据或重复
真正容易被忽略的点是:索引是否真的被用上。别只看有没有建索引,要跑 EXPLAIN 看 type 是不是 range 或 index,key 是否命中你建的那个,rows 是否接近你预期的 size 值——如果 rows 还是百万级,说明优化没生效,得回头检查索引顺序或条件下推位置。











