MySQL深分页慢因OFFSET导致全扫描前N行,优化需弃用页码、改用游标分页(如WHERE id > last_id),配合覆盖索引与合理复合索引设计。

直接用 LIMIT offset, size 查千万级数据的第 1000 页,基本等于让数据库干完活再扔掉 99.8% 的结果——不是慢,是根本没走对路。
为什么 OFFSET 越大,查询越慢
MySQL 执行 LIMIT 1000000, 20 时,并不会“跳到第 1000001 行开始读”,而是:先按 ORDER BY 排序,再从头扫描 1000020 行,最后丢弃前 1000000 行。即使 id 有索引,EXPLAIN 显示 rows 仍为百万级,Extra 常为 Using filesort 或空(说明没跳过扫描)。
- 排序字段没索引?查询直接退化成全表扫描
- 索引存在但顺序不匹配?比如
ORDER BY created_at DESC, id DESC,而索引是(created_at, id)(ASC 默认),就无法有效利用 - WHERE 条件 + ORDER BY 字段混用时,复合索引必须把过滤字段放最左,排序字段紧随其后,否则索引失效
游标分页(Cursor-based Pagination)怎么写
核心是放弃“第 N 页”思维,改用“从某条记录之后取下一批”。它依赖一个**唯一、有序、非空**的字段(首选主键 id,或 created_at + id 组合)。
- 升序分页(如
ORDER BY id ASC):下一页查WHERE id > ? LIMIT 20 - 降序分页(如
ORDER BY id DESC):下一页查WHERE id - 时间字段可能重复?必须补唯一字段,例如
ORDER BY created_at DESC, id DESC,游标传两个值:created_at = '2024-05-01 10:23:45'和id = 987654,下一页条件为WHERE (created_at, id) - 前端必须保存并透传游标值;首次请求可不带
WHERE,或用WHERE id > 0启动
索引设计不只为了 WHERE
深分页场景下,索引目标不是“快速过滤”,而是“让数据库能用索引完成排序 + 范围定位”,即覆盖整个查询路径。
- 单字段排序(如
ORDER BY id DESC):索引建在(id)即可,方向需与查询一致(DESC索引要显式声明INDEX idx_id_desc (id DESC),MySQL 8.0+ 支持) - 带 WHERE 条件(如
WHERE status = 'paid' ORDER BY created_at DESC):建复合索引(status, created_at),注意status区间越窄(如枚举值少),索引效率越高 - 避免回表:如果只查
id和created_at,索引可建为(status, created_at, id),成为覆盖索引,EXPLAIN中Extra会显示Using index - 别迷信
COUNT(*):InnoDB 下它要遍历索引树,千万级表可能秒级延迟,且结果只是快照;改用LIMIT 21判断第 21 行是否存在,决定是否显示“下一页”
真正卡住性能的往往不是 SQL 写法本身,而是把“页码”当成了接口契约——一旦前端坚持传 page=1000,后端除了硬扛或拒绝,几乎没有优雅解法。游标值看起来多传了一个参数,但它把状态从服务器悄悄转移到了客户端,这才是大规模数据分页能落地的关键隐含前提。

















