limit offset 偏移量大时变慢是因mysql需扫描并丢弃前n行;id范围查询(如where id > last_id)可跳过扫描,性能提升数十倍;游标分页需单调递增且有索引的排序字段,配合order by create_time desc, id desc防重复。

LIMIT OFFSET 在偏移量大时会变慢,不是语法问题,是 MySQL 执行机制决定的——它必须扫描并丢弃前 N 行。ID 范围查询(如 WHERE id > last_id)能跳过扫描,性能差距可达几十倍。
为什么 LIMIT offset, rows 越往后越慢
MySQL 并不会“直接跳到第 100000 行”,而是:从第一条记录开始逐行扫描、计数,直到累计满足 offset + rows 条才停止,再丢弃前 offset 条。即使有主键索引,OFFSET 仍需回表或遍历索引树做计数。
- 执行
EXPLAIN SELECT * FROM orders WHERE type = 8 LIMIT 100000, 20通常显示type: ALL或rows接近全表,Extra含Using where甚至Using filesort - 偏移量每增加 10 倍,耗时并非线性增长,而是接近指数级爬升(例如 10 万偏移 vs 100 万偏移,可能从 300ms → 4s)
- ORDER BY 字段无索引、或排序字段与 LIMIT 不一致时,性能恶化更剧烈
用 WHERE id > last_id 替代 OFFSET 的前提和写法
这是游标分页(cursor-based pagination),核心是“记住上一页末尾 ID”,不再依赖行号计数。
- 必须确保排序字段(如
id)是**单调递增、不重复、有索引**的;若用时间字段(create_time),需加id作二级排序防重复:ORDER BY create_time DESC, id DESC - 第一页查法不变:
SELECT * FROM orders ORDER BY id ASC LIMIT 20 - 第二页起传入上一页最后一条的
id(比如是1024):SELECT * FROM orders WHERE id > 1024 ORDER BY id ASC LIMIT 20 - 不能跳页(如从第 1 页直接到第 100 页),但适合无限滚动场景,且天然避免数据错位(新插入记录不会导致“漏条”)
WHERE id >= (SELECT id FROM ... LIMIT offset, 1) 的适用边界
这是子查询优化法,试图保留“跳页”能力的同时绕过全扫描,但它只是把性能瓶颈从主查询转移到子查询里。
- 子查询
(SELECT id FROM orders ORDER BY id LIMIT 100000, 1)本身仍要扫描 100001 行,只是不返回完整数据 - 当
offset超过 10 万,该子查询已明显变慢,实测比纯LIMIT快不了多少(仅 2–3 倍),远不如游标分页 - 唯一优势:业务逻辑无需改造(仍可传页码),适合无法改前端、又不愿加缓存的过渡方案
- 务必给
ORDER BY字段建联合索引,否则子查询可能走全表
容易被忽略的细节:排序稳定性与数据一致性
无论选哪种方案,只要排序依据不是唯一键,就可能因并发写入导致同一页两次查询结果不一致(例如两条记录 create_time 相同,MySQL 返回顺序不确定)。
- 游标分页中,若只用
create_time DESC,新插入的同时间记录可能插在中间,造成“重复出现”或“丢失” - 正确做法是追加一个唯一字段兜底:
ORDER BY create_time DESC, id DESC,且查询条件也要对应:WHERE create_time -
LIMIT分页没有这个问题——它只认行号,但代价是性能不可控;游标分页有确定性,但要求客户端严格传递游标值











