在MySQL查询中,LIMIT子句是最常用的分页手段,但在真实业务里,随着翻页深度增加,响应时间会显著变慢。理解其底层执行逻辑,才能针对性地做优化。

LIMIT分页的性能瓶颈在哪里
MySQL处理LIMIT offset, size时,执行引擎会先根据查询条件(或全表扫描)读取记录,每读一条就累加计数器,直到跳过offset条后才开始返回size条数据。这意味着offset越大,被临时丢弃的行越多。如果表没有合适索引,或者索引无法覆盖排序与过滤,就会触发大量的随机IO与回表操作。
我们可以通过EXPLAIN观察这类语句。对于SELECT * FROM orders ORDER BY id LIMIT 100000, 20;,即便id是主键,MySQL仍要在索引上顺序定位到第100001条记录,再回表取全部字段。虽然主键索引让“定位”比全表扫描快,但前十万次索引遍历与丢弃动作依旧不可避免。当并发稍高,这种深分页就会成为慢查询主力。
常规优化方案:主键游标分页
如果业务允许基于有序主键连续翻页,可改用“上一页最大id”作为游标,从而避免offset。这种方式把跳跃式扫描变成范围查找,数据库直接从上次位置开始读。
例如原深分页语句:
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
改为游标方式:
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
此写法中,id > 100000让存储引擎用B+树快速定位起点,仅需顺序读20条。它彻底消除了丢弃前十万行的开销,延迟恒定。缺点是只能做“下一页”而不能随意跳到第N页,适合信息流、日志浏览等场景。
延迟关联优化深分页
当必须支持任意页码跳转,且查询包含复杂条件或需回表取多列时,可采用延迟关联。核心思路是先通过索引查出目标行的主键,再用主键回表取完整数据,从而把回表量从offset+size降到size。
假设用户表有复合索引(status, create_time),需要按状态分页查详情:
SELECT a.* FROM users a INNER JOIN ( SELECT id FROM users WHERE status = 1 ORDER BY create_time LIMIT 100000, 20 ) b ON a.id = b.id;
子查询只在索引(status, create_time)上遍历并丢弃十万行,由于索引叶子节点含id,无需回表;外层用20个id精确回表。相比SELECT * FROM users WHERE status=1 ORDER BY create_time LIMIT 100000,20少回表十万次,CPU与IO明显下降。该方案仍受offset扫描影响,但把最重的回表代价压到最低。
其他辅助手段与边界
除了上述两种,还可利用覆盖索引减少IO,或预计算页码映射。若数据接近静态,可冗余存储行号分段。需要留意:延迟关联要求子查询索引能覆盖排序与过滤;游标分页则依赖不出现空缺断号的连续主键,且不支持按热度等乱序跳页。
在分库分表环境下,LIMIT更需小心,各分片独立深翻页再归并会放大开销,此时常改用基于分片键的游标或搜索引擎承接。总之,优化LIMIT分页的本质是减少无效扫描与回表,按业务形态选择游标或延迟关联,才能兼顾体验与成本。
| 方案 | 适用场景 | 跳页能力 | 主要收益 |
|---|---|---|---|
| 主键游标 | 连续浏览、信息流 | 仅下一页 | 消除offset丢弃 |
| 延迟关联 | 任意跳页、多列返回 | 支持 | 降低回表行数 |
| 覆盖索引 | 仅需索引列 | 支持 | 减少IO |
分页优化没有银弹,先确认业务是否真需要深跳页,再决定用游标还是延迟关联,避免过早复杂化。