结论:MySQL执行LIMIT offset,size必须顺序扫描并丢弃前offset行,导致扫描行数=offset+size,I/O随offset线性激增;最优解是用主键条件(WHERE id>last_id)替代OFFSET,或覆盖索引+延迟关联,二者直接避免全量扫描。

直接说结论:用主键条件替代 LIMIT offset, size,或改用覆盖索引 + 延迟关联,是减少扫描行数最有效的两种方式。其他方案(如游标、缓存、预计算)都是围绕这两个核心思路的延伸或补充。
为什么 LIMIT offset, size 扫描行数会爆炸?
MySQL 并不支持“跳到第 N 行”这种随机定位操作。执行 SELECT * FROM orders ORDER BY id LIMIT 1000000, 20 时,它必须:
- 先按
id排序所有匹配行(如果没索引,就是全表 + filesort) - 顺序读取前 1000020 行
- 丢弃前 1000000 行
- 只返回最后 20 行
也就是说,offset 每增加 1 万,扫描行数就多 1 万 —— 这不是查询变慢,是 MySQL 在做大量无意义 I/O。
用主键条件替代 OFFSET:简单但有前提
适用于按主键(或唯一、有序字段)升序/降序分页,且业务允许“下一页”连续翻页。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 第一页:
SELECT * FROM orders ORDER BY id ASC LIMIT 20 - 第二页(已知上页最大
id = 105):SELECT * FROM orders WHERE id > 105 ORDER BY id ASC LIMIT 20 - 必须确保
id字段有索引(通常是主键,天然满足) - 不能用于跳转到任意页码(比如用户输“第 5000 页”),因为无法快速算出起始
id - 降序同理,用
WHERE id ,注意边界值重复问题(如并发插入)
覆盖索引 + 延迟关联:通用性最强的优化
当排序字段不是主键(比如按 create_time 分页),又必须查完整行时,这是首选。
- 先建联合索引:
ALTER TABLE orders ADD INDEX idx_ctime_id (create_time, id) - 子查询只走索引取主键:
SELECT id FROM orders ORDER BY create_time DESC LIMIT 1000000, 20 - 外层用主键回表:
SELECT o.* FROM orders o INNER JOIN (子查询) tmp ON o.id = tmp.id - 关键点:子查询不查任何非索引列,避免回表;外层只回表 20 次,而不是 1000020 次
- 用
EXPLAIN验证子查询的type是index或range,且Extra不含Using filesort和Using temporary
游标分页:适合高并发实时列表,但设计成本略高
本质是把“页码”换成“上次最后一条的业务锚点”,彻底规避 offset。
- 客户端首次请求不带游标:
SELECT * FROM orders ORDER BY create_time DESC, id DESC LIMIT 20 - 服务端返回结果的同时,附带最后一条的
create_time和id(如"2026-10-12 14:30:00", 987654) - 下一页请求传入:
SELECT * FROM orders WHERE create_time - 好处:无偏移开销、结果稳定(新数据插入不会导致翻页漏项或重复)
- 坑点:需要处理时间相同、id 相同的边界;客户端必须保存并传递两个字段;不适合后台导出类场景
真正容易被忽略的是:索引设计必须和查询模式严格对齐。比如用 ORDER BY create_time DESC,却只建了 INDEX(create_time) 升序索引,MySQL 8.0 前大概率退化为 filesort。还有,SELECT * 在延迟关联里看似方便,但若字段太多,网络传输和内存拷贝本身就会成为瓶颈 —— 能明确字段就别偷懒。

















