深分页性能线性退化是mysql底层执行逻辑所致,应避免直接使用limit offset,size;推荐子查询+主键关联或游标分页(where id > ?)优化,前提是排序字段必须有索引且尽量避免select *。

直接用 LIMIT offset, size 查深分页,性能会线性退化——这不是配置问题,是 MySQL 底层执行逻辑决定的。偏移量越大,它越要扫描、排序、丢弃越多无效行,哪怕有索引也救不了。
为什么 LIMIT 1000000, 20 比 LIMIT 0, 20 慢几十倍?
MySQL 不会“跳”到第 1000001 行,而是老老实实从头开始扫描,计数到 1000020 行,再丢掉前 1000000 行。这个过程:
- 即使 ORDER BY id 走主键索引,也要逐条读取并计数;
- 如果 SELECT *,每丢弃一行,都可能触发一次回表(随机 I/O);
- 若排序字段无索引,还会触发 filesort,内存不够就写磁盘临时文件;
- 偏移量每增加 10 万,耗时基本等比例上涨,不是“变慢一点”,是“断崖式变慢”。
用子查询 + 主键关联替代直接 LIMIT offset, size
核心思路是把“扫描百万行再丢弃”变成“只定位目标 ID,再精准查数据”。前提是排序字段(如 id)必须有索引(主键最优)。
示例(假设查第 50 万页,每页 20 条):
SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders ORDER BY id LIMIT 4999999, 20 ) tmp ON t.id = tmp.id;
这样做的关键点:
- 子查询 SELECT id FROM ... LIMIT ... 只走主键索引,不回表,极快;
- 外层 JOIN 是基于主键的等值匹配,B+ 树查找效率高;
- 避免了全字段扫描和大量随机 I/O;
- 注意:不能用 IN 替代 JOIN,因为 MySQL 不允许 IN 子句里带 LIMIT。
改用游标分页(WHERE id > ?)彻底避开 offset
适用于用户“向下翻页”场景(如无限滚动),不再依赖页码,而是记住上一页最后一条的 id 值。
首次请求:
SELECT * FROM orders ORDER BY id LIMIT 20;
假设返回的最后一条 id = 1005,下一页就是:
SELECT * FROM orders WHERE id > 1005 ORDER BY id LIMIT 20;
优势非常明显:
- 每次都是范围扫描 + 索引查找,复杂度稳定在 O(log n);
- 不受总数据量或当前页码影响,第 1 万页和第 1 页响应时间几乎一致;
- 缺陷也很明确:不能跳转任意页码(比如直接点“第 88 页”),且要求排序字段严格唯一;
- 如果排序字段可能重复(如 update_time),得组合使用:WHERE (update_time, id) > (?, ?)。
真正卡住人的往往不是方案选哪个,而是没意识到 OFFSET 本身是个“伪优化”——它看起来简单,却在数据量上来后把性能漏洞暴露得最彻底。游标分页需要前端配合,子查询方案能兼容旧接口,但两者都绕不开一个前提:排序字段必须走索引,且尽量避免 SELECT *。











