一、面试题
先来看一个经典的面试场景:在电商后台的订单列表中,用户按时间倒序分页查询,每页10条。当翻到第10000页时,SQL突然变得奇慢无比——这背后的原因是什么?又该怎么分析和优化?

二、真实业务场景
假设订单表里躺着5000万条数据,后台需要查询最近的订单,SQL大致如下:
SELECT id, order_no, user_id, amount, status, created_atFROM ordersWHERE status = 1ORDER BY id DESCLIMIT 999900, 10;
注意,LIMIT 999900, 10 并不是直接跳到第999901条数据——它会先扫描、排序,然后跳过前999900条记录,最后才返回10条。页码越大,扫描的数据量就越大,性能自然直线下降。这就是MySQL深度分页性能问题的根源。
三、方案一:子查询优化
既然瓶颈在于大量无效扫描,那能不能先只查主键,再回表拿完整数据?答案是肯定的。利用覆盖索引,先找出当前页的主键:
SELECT id, order_no, user_id, amount, status, created_atFROM ordersWHERE id IN ( SELECT id FROM orders WHERE status = 1 ORDER BY id DESC LIMIT 999900, 10)ORDER BY id DESC;
当然,别忘了给status和id建联合索引:
CREATE INDEX idx_status_idON orders(status, id);
子查询只取id,可以尽量利用覆盖索引,减少回表次数。这种子查询优化方案适合需要跳转到指定页码的场景(比如用户直接点第10000页),但深度很大时,子查询内部仍然要扫描前面的数据,性能提升有限。
四、方案二:基于游标或最大 ID 分页
如果业务是连续翻页(比如无限滚动加载),那就不必每次都重新计算偏移量了。直接记住上一页最后一条记录的ID,下一页以此为基础继续往下查。第一次查询:
SELECT id, order_no, user_id, amount, status, created_atFROM ordersWHERE status = 1ORDER BY id DESCLIMIT 10;
假设返回的最小ID是985000,下一页就这样写:
SELECT id, order_no, user_id, amount, status, created_atFROM ordersWHERE status = 1 AND id < 985000ORDER BY id DESCLIMIT 10;
同样需要索引:
CREATE INDEX idx_status_idON orders(status, id);
这种方式直接从索引的指定位置开始读取,完全不需要扫描丢弃前面的数据,性能基本不受页码影响。但缺点也很明显:它不支持随机跳页,只适合连续翻页。
五、排序字段不唯一时的写法
如果排序字段不是主键,比如按created_at倒序,多个订单可能撞时间,这时候就需要再加一个唯一字段(比如id)来保证排序稳定。写法如下:
SELECT id, order_no, user_id, amount, status, created_atFROM ordersWHERE status = 1 AND ( created_at < '2026-07-16 10:00:00' OR ( created_at = '2026-07-16 10:00:00' AND id < 985000 ) )ORDER BY created_at DESC, id DESCLIMIT 10;
对应的索引应该是:
CREATE INDEX idx_status_created_idON orders(status, created_at, id);
这样created_at和id就组成了一个稳定的游标,避免了数据重复或遗漏,是游标分页处理非唯一排序字段的规范写法。
六、方案三:使用 Elasticsearch
如果业务场景是商品搜索、日志检索这种复杂查询,MySQL可能扛不住,这时候Elasticsearch就派上用场了。但传统分页用from/size同样有深度分页问题:
{ "from": 999900, "size": 10}
ES内部也需要维护大量搜索结果,深度页码性能依然堪忧。推荐使用search_after:
{ "size": 10, "query": { "term": { "status": 1 } }, "sort": [ { "created_at": "desc" }, { "id": "desc" } ], "search_after": [ "2026-07-16T10:00:00", 985000 ]}
search_after需要携带上一页最后一条记录的排序值,同样只适合连续翻页,但能有效避免Elasticsearch中的深度分页性能问题。
七、方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
LIMIT offset,size | 写法简单 | 页码越大越慢 | 数据量小 |
| 子查询 | 减少回表数据 | 深度很大时仍需扫描 | 需要页码跳转 |
| 游标分页 | 性能稳定 | 不支持随机跳页 | 无限滚动、连续翻页 |
| Elasticsearch | 支持复杂搜索 | 需要维护数据同步 | 搜索、日志、订单检索 |
八、面试总结
解决MySQL深度分页问题,核心不是简单修改LIMIT,而是减少数据库需要扫描和丢弃的数据量。实际项目中,通常这样选型:
- 数据量较小:直接使用
LIMIT,简单粗暴,适合入门场景。 - 必须支持页码跳转:使用子查询 + 覆盖索引,虽然深度大时仍有损耗,但比直接
LIMIT好很多,是常见的MySQL分页优化手段。 - 连续翻页或无限滚动:使用基于ID或时间的游标分页,性能稳定,是最常见的选择,尤其适合大数据量下的连续翻页。
- 复杂搜索场景:直接上Elasticsearch的
search_after,让搜索引擎分担压力,避免数据库深度分页瓶颈。 - 排序字段不唯一:记得用“排序字段 + 主键”构造稳定游标,避免数据重复或遗漏,这是游标分页的进阶技巧。
以上就是MySQL解决深度分页问题的几种主流方案。理解了背后的原理,再面对类似的面试题或实际痛点,就能从容应对了。
