在开发管理后台、ERP 系统或信息流页面时,分页查询是最基础的功能。绝大多数开发人员习惯于使用 MySQL 的 LIMIT OFFSET, SIZE 语法来实现。
在项目初期,数据量小,这种写法没有任何问题。但当单表数据量突破百万,且用户试图翻到第 1 万页时,接口响应时间会从几毫秒飙升到十几秒,甚至直接把数据库跑挂。这种现象在工程上被称为**“深度分页(Deep Pagination)”瓶颈**。
本文将拆解 LIMIT 语法在底层引擎的执行逻辑,并给出生产环境中解决深度分页的三种标准优化方案。
一、 为什么 LIMIT 1000000, 20 会这么慢?
假设我们有一张千万级的订单表 orders,为了按时间排序分页,我们在 create_time 字段上建了索引。
业务代码执行了这样一条 SQL,试图获取第 5 万页的数据
SELECT * FROM orders ORDER BY create_time LIMIT 1000000, 20;
底层执行的“灾难”逻辑: 很多开发者误以为,数据库会直接跳到第 1000000 行,然后拿出 20 条数据返回。事实并非如此。
结合上一篇文章中“回表”的概念,MySQL 的真实执行轨迹是:
- 引擎在 create_time 这个二级索引树上,从头开始按顺序扫描 1,000,020 个节点,拿到这些节点对应的主键 ID。
- 致命动作来了: 引擎拿着这 1,000,020 个主键 ID,去主键聚集索引树里执行了 1,000,020 次回表查询,把每一行的完整数据全部查出来,放进内存。
- 引擎将前面 1,000,000 行完整数据无情丢弃(Offset 阶段)。
- 仅保留最后的 20 行数据返回给客户端。
结论: 深度分页慢,不是慢在扫描二级索引,而是慢在为了那被丢弃的 100 万条数据,执行了毫无意义的 100 万次随机磁盘 I/O(回表)。大量的无用数据还会瞬间把 Buffer Pool(内存缓冲池)里的热数据挤走。
二、 优化方案一:延迟关联(Deferred Join)
既然慢在“回表”,我们的核心思路就是:尽可能晚地进行回表,并且只为最终需要的那 20 条数据回表。
这就需要用到“覆盖索引(Covering Index)”的特性。如果在二级索引树上就能拿到查询想要的所有字段,数据库就不会去回表。主键 ID 天然就存在于二级索引的叶子节点中。
优化后的 SQL(子查询方式):
SELECT o.* FROM orders o
INNER JOIN (
-- 第一步:利用覆盖索引,只查主键,绝对不回表。这一步极快!
SELECT id FROM orders ORDER BY create_time LIMIT 1000000, 20
) AS tmp ON o.id = tmp.id; -- 第二步:拿着最后精准定位的 20 个主键,再去原表拿所有字段。
适用场景: 绝大多数需要跳页查找(比如用户直接在分页器输入跳转到第 5000 页)的 B 端管理后台场景。代码改动极小,性能提升在 10 倍以上。
三、 优化方案二:基于游标的游走翻页(Seek Pagination)
如果你开发的不是后台管理系统的表格,而是类似微博、抖音、移动端电商列表的**“无限下拉刷新(瀑布流)”**,用户其实只能一页一页往下翻,不能直接跳页。
这种场景下,可以直接废弃 OFFSET,采用基于最大 ID 或时间戳的游标方案。
优化逻辑: 前端每次请求下一页时,必须把上一页最后一条数据的特征值(通常是自增 ID 或排序时间)传给后端。
优化后的 SQL:
-- 假设前端传来的上一页最后一条记录的 create_time 是 '2026-04-10 14:00:00'
SELECT * FROM orders
WHERE create_time < '2026-04-10 14:00:00'
ORDER BY create_time DESC
LIMIT 20;
原理: 因为有了具体的 WHERE 条件,数据库直接利用 B+ 树的二分查找特性,瞬间定位到该时间点,然后往后拿 20 条即可。不管翻到第几千万页,查询时间永远是 O(1) 级别的毫秒级响应。
适用场景: 无限下拉瀑布流、定时任务批量拉取数据。 注意:排序字段必须保证唯一性(如果是时间,建议加上 ID 作为第二排序条件 ORDER BY create_time DESC, id DESC,防止同一秒的数据出现分页丢失)。
四、 优化方案三:业务妥协与架构转移
技术优化是有边界的。如果产品经理坚持要求:在拥有 5000 万数据的 C 端用户订单列表里,提供复杂的复合条件筛选,并且必须支持输入任意页码跳转。
作为后端开发,你应该明确指出:关系型数据库不适合做海量数据的深度检索。
此时不应该再在 MySQL 的 SQL 上死磕,而应该引入搜索引擎机制。 将查询条件和用于排序的维度数据,通过 CDC(如 Canal)同步到 Elasticsearch (ES) 或者 ClickHouse 中。 业务的分页请求直接打向 ES 获取主键 ID,然后再回 MySQL 查详情,这才是处理海量复杂分页的标准架构。
五、 总结
写业务代码时遇到分页需求,不要无脑 LIMIT M, N。
- 几十万级以内: 直接用原生 LIMIT 没问题。
- 百万级大表 + 跳页需求: 必须使用延迟关联(子查询 JOIN 主键)。
- 瀑布流 / 跑批拉取: 彻底抛弃 OFFSET,使用最大值游标翻页。
- 千万级 + 复杂检索: 把查询职责交接给 Elasticsearch。