MySQL中LIMIT分页随offset增大而变慢,是因为其必须扫描并丢弃前offset行,而非真正跳过;实际扫描行数为offset+limit,I/O和CPU开销线性增长,时间复杂度O(offset+limit),游标分页可规避此问题。

因为 MySQL 执行 LIMIT offset, size 时必须扫描并丢弃前 offset 行,不是跳过——哪怕只取 10 条,offset 是 100 万,它就得读满 1000010 条,再扔掉前 100 万。
MySQL 的 LIMIT offset 实际执行过程是“扫+丢”,不是“跳”
很多人以为 MySQL 能直接定位到第 N 行。事实是:它从索引起点开始,逐行计数,直到凑够 offset + limit 条才停。被丢弃的那些行,依然要走存储引擎、进缓冲池、触发回表(如果没覆盖索引)、甚至写临时文件(如果排序无法内存完成)。
-
EXPLAIN中的rows值会显示为offset + limit,不是limit - 即使
id是主键,ORDER BY created_at LIMIT 1000000, 20也大概率不走id索引,除非created_at有对应索引 - 优化器可能判断“全表扫描比走索引回表更快”,于是降级为
type: ALL,Extra出现Using filesort
延迟关联(Deferred Join)为什么能提速?
核心是把“查全字段 + 排序 + 跳过”拆成两步:先用轻量子查询只走索引拿 id,再用这些 id 精准回表。避免大字段和非索引列拖慢扫描路径。
- 子查询必须只选索引列(如
id或复合索引覆盖列),且ORDER BY字段需在该索引中 - 外层
JOIN必须用主键或唯一键匹配,否则 MySQL 8.0.22 之前会报错 - 不能用
IN (SELECT ... LIMIT)替代——MySQL 明确禁止子查询带LIMIT的IN - 示例:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY create_time DESC LIMIT 1000000, 20) AS tmp ON o.id = tmp.id
游标分页(Keyset Pagination)为何性能恒定?
它彻底去掉 OFFSET,用上一页最后一条记录的排序值作为下一页的 WHERE 起点。MySQL 只需从索引中找到那个值的位置,往后取 LIMIT 条,时间复杂度 O(1)。
- 要求排序字段组合必须唯一且单调(如
(create_time DESC, id DESC)),否则可能漏数据或重复 - 前端必须保存上一页末尾的完整排序键(不止一个字段),比如
create_time = '2025-06-30' AND id = 987654 - 不能跳页(比如直接翻到第 100 页),只适用于“下一页”“上一页”或无限滚动场景
- 典型写法:
SELECT * FROM orders WHERE create_time
真正容易被忽略的是:索引设计必须匹配查询模式。比如用 create_time 分页,却只给 id 加了主键索引——那延迟关联和游标分页都起不了作用。优化不是换写法就行,得让索引、排序、查询条件三者对齐。


















