深度分页慢的本质是“跳过”成本高,mysql需顺序扫描offset+size行;应改用主键/唯一索引条件过滤替代offset,或通过子查询先分页再join。

深度分页为什么慢?本质是“跳过”成本高
MySQL 的 LIMIT offset, size 在 offset 很大时(比如 LIMIT 1000000, 20),数据库仍需从头扫描并跳过前 100 万行,即使只返回 20 条。InnoDB 没有跳转索引指针的能力,只能顺序遍历聚簇索引或覆盖索引——扫描行数 = offset + size,I/O 和 CPU 开销陡增。
用主键/唯一索引+条件过滤替代 OFFSET
核心思路:不跳行,而是“记住上一页末尾值”,下一页查询直接从该值之后取数据。要求排序字段有唯一性(推荐主键或带唯一约束的列)。
- 原语句:
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20 - 改写后:
SELECT * FROM orders WHERE id > 1234567 ORDER BY id LIMIT 20(假设上一页最后 id 是 1234567) - 必须确保 WHERE + ORDER BY 字段命中索引,且排序方向一致(如都用 ASC)
- 若排序字段非唯一(如 create_time),需组合去重字段:
WHERE (create_time, id) > ('2024-01-01', 999999) ORDER BY create_time, id LIMIT 20
子查询拆分:先缩小结果集再关联
当分页涉及多表 JOIN 或复杂 WHERE 条件时,直接分页会导致全量 JOIN 后再截断,浪费巨大。应将分页逻辑“推到内层”,只对关键驱动表做分页,再补关联字段。
- 低效写法:
SELECT o.*, u.name FROM orders o JOIN users u ON o.uid = u.id WHERE o.status = 1 ORDER BY o.id LIMIT 100000, 20(先 JOIN 百万行,再截断) - 优化写法:
SELECT o.*, u.name FROM (SELECT id, uid FROM orders WHERE status = 1 ORDER BY id LIMIT 100000, 20) o JOIN users u ON o.uid = u.id(先按索引快速定位 20 条订单 ID,再精准关联) - 注意:子查询中必须包含所有外层需要的关联键(如 uid),且 ORDER BY 和 LIMIT 必须在子查询内完成
其他实用补充策略
不是所有场景都适合游标分页,以下情况可辅助使用:
-
延迟关联(Delayed Join):先查主键,再用 IN 关联详情(适用于 ID 数量不多时)
SELECT * FROM orders WHERE id IN (SELECT id FROM orders WHERE status=1 ORDER BY id LIMIT 100000,20) - 物理分表 + 分页路由:按时间或用户 ID 拆分大表,分页请求定向到具体子表,避免单表过大
- 缓存分页结果:对读多写少、时效性要求不高的列表(如后台运营页),缓存固定 offset 的结果集(注意缓存穿透与更新一致性)











