limit offset, size 越往后越慢是因为mysql必须扫描前offset行才能定位数据,i/o与cpu成本线性上升;应优先用游标分页(基于有序字段如id过滤)和覆盖索引优化,仅在需跳页时限制offset范围或降级处理。

为什么 LIMIT offset, size 越往后越慢
因为 MySQL 在执行 LIMIT 100000, 20 这类语句时,必须先扫描前 100000 行(哪怕不返回),再取后面 20 行。索引能跳过全表扫描,但无法跳过“跳过 N 行”这个动作——offset 越大,引擎越要按索引顺序逐条计数,I/O 和 CPU 成本线性上升。
常见错误现象:EXPLAIN 显示 rows 高达几十万,Extra 列出现 Using filesort 或空值(说明没用上覆盖索引)。
- 使用场景:后台列表页、数据导出、后台分页接口,尤其当总记录数超百万时
- 根本原因不是“没建索引”,而是“索引没被用来跳过 offset”
- 如果排序字段无索引,或
ORDER BY和WHERE字段组合不匹配索引最左前缀,索引直接失效
用游标分页替代 LIMIT offset, size
核心思路:不依赖行号,改用上一页最后一条的主键(或唯一、有序字段)作为下一页起点。MySQL 可以用索引直接定位,跳过所有前面的行。
假设按 id 升序分页,第一页查 SELECT * FROM t ORDER BY id LIMIT 20,拿到第 20 条的 id = 12345,第二页就写成:
SELECT * FROM t WHERE id > 12345 ORDER BY id LIMIT 20
这样 MySQL 直接在索引中二分查找 id > 12345 的第一条,然后往后取 20 行,完全避开 offset 扫描。
- 必须有可用于排序和过滤的单调字段(推荐主键
id,或带索引的created_at) -
WHERE条件必须和ORDER BY字段一致,否则可能漏数据或重复 - 不能跳页(比如从第 1 页直接到第 100 页),只适合“下一页/上一页”场景
- 如果排序字段有重复值(如多个记录
created_at相同),需加二级排序字段(如ORDER BY created_at, id)并同步用于WHERE
联合索引设计要点:WHERE + ORDER BY + SELECT 三者对齐
分页快不快,最终取决于能不能走**覆盖索引 + 索引有序扫描**。单列索引往往不够,得靠联合索引把查询路径“焊死”。
例如查询:SELECT id, title, created_at FROM article WHERE status = 1 ORDER BY created_at DESC LIMIT 20
最优索引是:INDEX(status, created_at)(注意顺序)。如果还要避免回表,可扩展为:INDEX(status, created_at, id, title)(覆盖全部 SELECT 字段)。
- 最左前缀必须包含所有
WHERE等值条件字段(status = 1) - 紧接着放
ORDER BY字段(created_at),且方向一致(DESC要显式声明,MySQL 8.0+ 支持混合方向) - 最后补上
SELECT中的其他字段,构成覆盖索引,避免回表 - 别在索引里加
text或长varchar字段,会拖慢索引效率
什么时候还是得用 LIMIT offset, size?怎么减损
搜索页、管理后台支持任意跳转页码、或者业务要求显示“共 XXX 页”,这时候游标分页不适用,只能硬扛 offset。但可以控制伤害:
- 加
WHERE条件收窄范围(比如限定最近 3 个月数据),让offset基于更小的结果集计算 - 用
SQL_CALC_FOUND_ROWS已废弃,改用单独SELECT COUNT(*),但记得加同样WHERE,且该语句也需走索引 - 前端限制最大可跳页码(如最多到第 100 页),后端对超限请求直接返回空或提示
- 对超大偏移量(如
offset > 100000),主动降级为游标逻辑(比如转成 “从某个时间点之后查”)
真正难处理的是“既要跳页、又要全量统计、还要毫秒响应”——这种需求背后往往没想清数据规模和用户真实行为,得拉产品一起砍。











