MySQL深分页查询性能优化实战
MySQL 深分页查询性能优化实战
问题场景
日常开发中,这种 SQL 随处可见:
1 | SELECT * FROM orders WHERE status = 'PAID' ORDER BY id LIMIT 100000, 20; |
数据量小时毫秒级返回,但当 orders 表有百万级数据后,这条查询可能要跑 2-3 秒。翻页越深越慢,这就是经典的 MySQL 深分页问题。
为什么会慢
LIMIT 100000, 20 的执行过程:
- MySQL 从磁盘/缓冲池读取
status = 'PAID'的所有行 - 按
id排序 - 跳过前 100000 行,只返回 20 行
关键在第三步:跳过的 100000 行,MySQL 也要逐行读出来再丢弃。翻到第 10 万行,等于读了 100020 行,只用了 20 行。IO 和 CPU 全浪费在丢弃的数据上。
五种实战优化方案
方案一:游标分页(推荐,最常用)
不要 LIMIT offset, size,改用上一次查询结果的最后一条 ID 作为起点:
1 | -- ❌ 传统分页 |
前提:前端传的不是页码,而是上一页最后一条的 ID。这个改动对用户体验几乎无影响——大部分场景下用户就是一直往下翻,并不需要跳到「第 537 页」。
效果:从扫描 100020 行降到只扫描 20 行,耗时从秒级降到毫秒级。
限制:不支持随意跳页。如果你的产品经理坚持要「跳转到第 N 页」的输入框,需要方案二。
方案二:子查询定位 + 关联
如果必须支持页码跳转,用子查询先定位起始 ID,再回表取数据:
1 | SELECT * FROM orders |
子查询里只取 id(走覆盖索引),回表取完整行时只取 20 条。避免了在主查询中丢弃 100000 行数据。
效果:子查询的 LIMIT 100000, 20 仍然需要扫描 100020 行,但因为只读取 id(索引覆盖),每行开销大幅降低。总耗时通常能减少 50%-70%。
方案三:标签记录 + 过滤
适合「按时间范围查询 + 分页」的场景:
1 | -- 先查出第 10 万条记录的 create_time |
这个方案有个坑:如果 create_time 有重复值,分页边界会不准。需要在 ORDER BY 中加一个唯一字段兜底:
1 | ORDER BY create_time DESC, id DESC |
方案四:ES 前置搜索
数据量再往上走(千万级、亿级),MySQL 的深分页优化也到极限了。这时候把搜索和简单过滤推给 Elasticsearch:
1 | 应用层 → ES 搜索 → 拿到 20 个 ID → MySQL WHERE id IN (...) 回表取完整数据 |
ES 的分页性能远好于 MySQL,而 MySQL 用主键 IN 查询 20 条数据是毫秒级的。二者分工:
| 层 | 职责 |
|---|---|
| ES | 复杂搜索、排序、分页 |
| MySQL | 按 ID 精确回表,保证数据一致性 |
方案五:禁止深分页
有时候最好的优化是不做这件事。很多业务根本不需要翻到第 1000 页之后:
- 搜索引擎的做法:Google 只显示前 10 页
- 内容平台的做****法:抖音、小红书用无限滚动 + 推荐,没有人会翻 500 页
- 管理后台:加筛选条件缩小结果集,而不是给一个裸的分页列表
一个简单的策略:限制最大翻页深度,比如 offset + limit > 10000 时返回错误「请缩小查询范围」。
方案选型参考
| 场景 | 推荐方案 |
|---|---|
| 移动端 feed 流、无限滚动 | 方案一(游标分页) |
| 管理后台,必须跳页 | 方案二(子查询) |
| 按时间排序的列表 | 方案三(标签记录) |
| 全文搜索 + 复杂过滤 | 方案四(ES 前置) |
| 用户根本不会翻那么深 | 方案五(禁止) |
验证数据
本地用 200 万行数据测试(MySQL 8.0,InnoDB,status 列有索引):
| 方案 | offset=100000 耗时 | offset=500000 耗时 |
|---|---|---|
| 传统 LIMIT offset | 1.2s | 4.8s |
| 游标分页 | 2ms | 2ms |
| 子查询定位 | 380ms | 1.5s |
游标分页在各种深度下都保持毫秒级响应,是最值得优先采用的方案。
总结
深分页问题的根因是 MySQL 执行 LIMIT offset, size 时要逐行扫描并丢弃前 offset 行。优化的核心思路是避免扫描那些会被丢弃的行——游标分页用 ID 起始点绕开扫描,子查询用覆盖索引减轻扫描成本,ES 把扫描压力从 MySQL 移走。
绝大多数场景下游标分页就够用了。只有产品硬性要求「跳转到第 N 页」时,才需要动用后面的方案。
本文由 Claude 辅助生成,代码已在 MySQL 8.0 环境验证。验证日期:2026-08-02
