分页查询性能优化核心是避免大offset扫描,推荐主键游标分页(如WHERE id < 12345 ORDER BY id DESC LIMIT 20)和延迟关联(先查ID再JOIN),并限制前端页码、使用覆盖索引、异步统计总数。

分页查询在Web应用中极为常见,但随着数据量增长,LIMIT offset, size 方式容易成为性能瓶颈——尤其当 offset 很大时,MySQL 仍需扫描并跳过大量无关记录。优化核心思路是:避免全表/大范围扫描跳过数据,改用“基于游标的定位”或“延迟关联”等策略。
用主键/唯一索引做游标分页(推荐)
适用于按主键或有唯一、有序字段(如 created_at)排序的场景。不依赖 OFFSET,而是记住上一页最后一条记录的排序字段值,下一页查询从该值之后开始。
- 原写法(低效):
SELECT * FROM orders ORDER BY id DESC LIMIT 10000, 20 - 优化写法:
SELECT * FROM orders WHERE id < 12345 ORDER BY id DESC LIMIT 20(假设上一页最大 id 是 12345) - 优势:走索引范围扫描,响应时间稳定,与总页数无关
- 注意:必须有合适的索引(如
INDEX(id)),且排序字段不能有大量重复值;若需多字段排序(如ORDER BY status, id),条件需组合覆盖(如WHERE (status, id) < ('shipped', 9999))
延迟关联(Deferred Join)减少回表开销
当查询字段多、表大、且 LIMIT 偏移较大时,先用子查询只取主键,再关联原表获取完整数据,可显著降低 I/O 和内存消耗。
- 原写法:
SELECT id, name, amount, created_at FROM orders ORDER BY created_at DESC LIMIT 10000, 20 - 优化写法:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY created_at DESC LIMIT 10000, 20) t ON o.id = t.id - 原理:内层只扫描索引(
INDEX(created_at)),不读取行数据;外层精准回表,避免扫描 10020 行后丢弃前 10000 行 - 前提:必须有覆盖排序和查询条件的联合索引,例如
INDEX(created_at, id)
避免深分页 + 合理限制前端页码
技术手段之外,产品逻辑也需配合。用户极少真正翻到第 500 页,盲目支持高 offset 反而拖垮数据库。
- 后端强制限制最大
offset(如 ≤ 10000),超出则返回空或提示“已加载至最新” - 前端禁用或灰化“跳转到指定页”输入框,改用“下一页 / 上一页”按钮 + 滚动加载(infinite scroll)
- 对搜索类分页,可结合 ES 或预计算聚合结果,绕开 MySQL 深分页
其他实用技巧
根据场景灵活补充:
-
使用
SQL_CALC_FOUND_ROWS要谨慎:它会强制扫描全部匹配行,即使只取前几条。改用单独SELECT COUNT(*)(带相同 WHERE 条件),并加缓存 -
分库分表后分页更复杂:需各分片分别取 top N,再合并排序取最终结果(如
sharding-jdbc的MemorySortShardingPagination) -
统计总数可异步更新:如订单总数用定时任务或 binlog 计数器维护,避免每次分页都
COUNT(*)

















