LIMIT 100000,20 变慢是因为MySQL需扫描并跳过前100000行,即使只返回20行;偏移量越大,无效I/O和CPU开销越显著,导致响应飙升、慢查询增多、主从延迟加剧。

为什么 LIMIT 100000, 20 会变慢
MySQL 在执行 LIMIT offset, size 时,即使只返回 20 行,也必须先扫描并跳过前 100000 行——这些行可能被读入内存、经过 WHERE 过滤、排序(如果有 ORDER BY),最后才取后 20 条。偏移量越大,无效扫描越多,I/O 和 CPU 开销越明显。
常见现象包括:响应时间从几毫秒飙升到数秒、慢查询日志频繁出现、主从延迟加剧。
关键点在于:OFFSET 不是“跳过索引位置”,而是“跳过逻辑结果行数”——只要排序或过滤逻辑不能被索引完全覆盖,就无法避免回表或全扫描。
用游标分页替代 LIMIT offset
适用于有明确排序字段(如 id 或 created_at)且该字段有索引的场景。核心思路是:不依赖行号偏移,而用上一页最后一条记录的排序值作为下一页起点。
- 原写法(低效):
SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20 - 优化写法(高效):
SELECT * FROM orders WHERE id (假设上一页最大id是 123456) - 要求:排序字段必须唯一或组合唯一(否则可能漏/重),且查询条件能命中索引(
WHERE + ORDER BY字段最好共用一个联合索引) - 注意:不能直接跳转到任意页,只适合“下一页”“上一页”这类线性翻页;若需跳转,可先用覆盖索引查出目标页首条记录的主键值,再二次查询
WHERE id BETWEEN + 覆盖索引减少回表
当必须支持跳页(比如用户输入页码),且主键连续、无大范围删除时,可预估 ID 范围缩小扫描面。
例如:已知每页 20 条,想查第 5001 页(即第 100001–100020 条),若主键 id 大致均匀递增,可估算 ID 区间:WHERE id BETWEEN 105000 AND 115000,再配合 LIMIT 截断。
- 必须配合
ORDER BY id和PRIMARY KEY索引,否则估算失效 - 更稳妥做法是先用覆盖索引查 ID:
SELECT id FROM orders ORDER BY id LIMIT 100000, 1(仍慢,但只查 ID,数据量小),拿到 ID 后再SELECT * FROM orders WHERE id >= ? ORDER BY id LIMIT 20 - 覆盖索引(如
INDEX(status, created_at, id))能让SELECT id完全走索引,不回表,大幅降低LIMIT前半部分代价
物理分表 or 归档冷数据缓解压力
如果偏移量大源于单表数据量过大(比如千万级订单表),光靠 SQL 优化边际收益递减。
- 对历史数据做归档:把
created_at 的数据迁出,主表只保留热数据,LIMIT偏移实际作用于更小数据集 - 按时间/业务维度分表(如
orders_2024_q1,orders_2024_q2),分页请求带时间条件,天然落到子表,扫描基数下降 - 注意:分表后跨表分页(如查最近 100 条订单)需应用层合并,不能靠单条 SQL 解决
真正难处理的是既要随机跳页、又要高并发、还要全量数据在线——这时候得接受“第 10000 页”本身就是反模式,应在产品侧限制可跳转页码,或改用搜索+筛选代替无条件翻页。











