当offset超过1万,limit分页必须换方案——因mysql需扫描offset+size行并丢弃前offset行,导致io和cpu开销随offset线性增长,属底层机制硬伤。

直接说结论:当 OFFSET 超过 1 万,LIMIT 分页就该换方案了——不是调优能救的,是 MySQL 底层扫描机制决定的硬伤。
为什么 OFFSET 越大查询越慢
MySQL 执行 LIMIT 100000, 10 时,并不会“跳到第 100001 行直接读”,而是先按 ORDER BY 排好全量满足条件的行,再从头逐行计数、跳过前 100000 行,最后取 10 行。这过程要回表、排序、临时存储,IO 和 CPU 开销随 OFFSET 线性增长。
常见错误现象包括:
- 同一分页 SQL,第 1 页毫秒级,第 10000 页查 60 秒以上
-
EXPLAIN显示rows高达几十万,但Extra里有Using filesort或Using temporary - 数据库 CPU 持续打满,慢查询日志里反复出现高 offset 的
LIMIT
主键 WHERE 分页(适合滚动加载)
这是最常用、性能最好的替代方案,前提是排序字段是**有索引的唯一递增字段**(如 id 或 order_id)。
实操要点:
- 第一页用
LIMIT 10,不带OFFSET:SELECT * FROM `order` ORDER BY id DESC LIMIT 10 - 下一页传上一页最后一条的
id值(比如上一页最大id是 9990):SELECT * FROM `order` WHERE id - 必须确保
WHERE条件走索引——检查EXPLAIN的key列是否非NULL - 不能跳页(比如从第 1 页直接跳到第 100 页),只支持“下一页/上一页”语义
子查询覆盖索引分页(支持跳页但有代价)
当业务必须支持任意页码输入(如后台管理页),且数据量在十万级以内,可用此法缓解回表压力。
核心思路:先用子查询只走索引捞出主键,再关联原表取完整字段。
示例:
SELECT o.* FROM `order` o INNER JOIN ( SELECT order_id FROM `order` ORDER BY order_id DESC LIMIT 99990, 10 ) t ON o.order_id = t.order_id;
注意事项:
- 子查询里的
ORDER BY字段必须有索引,否则子查询本身又变慢 - 如果原表字段多、行体积大,仍可能因回表产生明显延迟
- 比纯
LIMIT offset, size快,但不如主键WHERE分页;超 50 万行后响应仍可能 >500ms
容易被忽略的排序确定性问题
哪怕用了主键分页,如果 ORDER BY 不够唯一,结果可能错乱或漏数据。例如:
写成 ORDER BY created_at DESC,但多个记录 created_at 相同,MySQL 可能每次返回顺序不同,导致某条记录在第 2 页和第 3 页重复出现,或彻底丢失。
正确做法:
- 强制加唯一字段兜底:
ORDER BY created_at DESC, id DESC - 所有分页场景的
ORDER BY至少含一个唯一列(通常是主键) - 前端传来的游标值(如上一页最后的
created_at和id)必须成对校验,避免边界歧义
真正的瓶颈从来不在语法怎么写,而在你有没有让数据库“知道确切从哪开始”。LIMIT + OFFSET 是描述“我要第几块”,而主键 WHERE 是告诉数据库“我从这个位置继续”。后者才是它真正擅长的随机定位。











