根本原因是MySQL执行LIMIT offset,size时必须顺序扫描并丢弃前offset行,导致扫描行数=offset+size;最优解是用主键条件(WHERE id>last_id)替代OFFSET,或采用覆盖索引+延迟关联。

MySQL的LIMIT分页查询越往后越慢,根本原因不是LIMIT本身,而是OFFSET值变大后,MySQL必须线性扫描并丢弃大量数据——哪怕你只要10条记录,它也得先读、排、扔掉前几万甚至几百万条。
为什么LIMIT offset, size会触发全量扫描?
执行SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20时,MySQL实际做的是:
- 从索引(或表)中顺序读取100020条完整记录(含所有字段)
- 在Server层对这100020条记录排序(即使
id有索引,若SELECT *导致无法覆盖,仍需回表) - 丢弃前100000条,只返回最后20条
这个过程不跳过任何中间行——OFFSET没有“寻址”能力,只有“计数+丢弃”逻辑。EXPLAIN里rows值会随OFFSET线性增长,就是最直接的证据。
ORDER BY字段有索引,为什么还是慢?
有索引 ≠ 能跳过前N行。常见误区:
- 索引只加速“找到起始位置”,但MySQL仍要从头遍历索引树,逐条计数到第100000个节点
- 如果
SELECT *且索引不覆盖(比如只有INDEX (created_at)),每跳过一条记录都可能触发一次回表,随机I/O雪崩 - 优化器可能因统计信息偏差或
OFFSET过大,主动放弃索引,改走全表扫描(type: ALL)
真正有效的索引必须是覆盖索引:例如WHERE status = ? ORDER BY created_at DESC,对应索引应为INDEX (status, created_at, id, title),把过滤、排序、查询字段全包进去。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
游标分页为什么能彻底规避这个问题?
游标分页把“跳过N行”转为“从某值开始查”,绕开OFFSET机制:
- 第一页:
SELECT * FROM orders WHERE status = 'paid' ORDER BY id DESC LIMIT 20 - 拿到最后一条的
id = 98765,第二页写成:SELECT * FROM orders WHERE status = 'paid' AND id
关键点:
- 每次都是索引范围扫描(
range),执行时间恒定,与总数据量无关 - 要求排序字段单调递增且唯一;若用
created_at,必须追加主键兜底:ORDER BY created_at DESC, id DESC,WHERE条件也要同步写成WHERE created_at - 不支持随机跳页,但无限滚动场景下这是最稳定、可扩展性最强的方案
最容易被忽略的其实是业务层配合:前端必须可靠传递上一页末尾的游标值,且不能假设id连续或永不删除——一旦有删改,游标值就可能失效,需要结合业务语义设计容错逻辑。










