offset越大查询越慢,因为mysql需真实扫描并丢弃前n行;应避免深度分页,改用游标分页或覆盖索引+主键关联优化。

为什么 OFFSET 越大,LIMIT 查询越慢
MySQL 的 OFFSET 不是跳过已扫描的行,而是真实地扫描并丢弃前 N 行。比如 SELECT * FROM orders ORDER BY id LIMIT 10000, 20,MySQL 会先按 id 排序,再逐行读取前 10020 行,只返回后 20 行——前 10000 行全白读了,还占 I/O 和 CPU。
如果没走索引或排序字段无索引,还会触发 Using filesort,性能雪上加霜。
- ORDER BY 字段必须有索引,且和 WHERE 条件能共用(最左前缀原则)
- 避免
SELECT *,只查必要字段,减少回表开销 - 不要用
OFFSET做“深度翻页”,10 万行之后基本不可控
用游标分页(Cursor-based Pagination)替代 OFFSET
核心思路:不依赖行号,改用上一页最后一条记录的排序键值作为查询起点。例如按 created_at DESC, id DESC 分页,第二页就从上一页末尾的 (created_at, id) 值继续查:
SELECT * FROM posts
WHERE (created_at, id) < ('2024-05-01 10:20:30', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;这个查询能命中联合索引 (created_at, id),全程走索引范围扫描,不依赖偏移量。
- 必须保证排序字段组合唯一(或加主键兜底),否则可能漏数据或重复
- 不能跳页(如直接跳到第 100 页),但对“下一页”场景极友好
- 前端需保存上一页末尾的游标值,而不是页码
覆盖索引 + 主键关联优化传统 LIMIT
当必须用页码(比如后台管理列表),可先用覆盖索引快速定位主键,再回表取数据:
SELECT p.* FROM posts p INNER JOIN ( SELECT id FROM posts WHERE status = 1 ORDER BY created_at DESC, id DESC LIMIT 10000, 20 ) AS tmp ON p.id = tmp.id;
子查询只查 id,若 status 和排序字段都在同一索引里(如 (status, created_at, id)),就能完全走索引,不回表;外层再用主键关联取完整数据,大幅减少扫描量。
- 索引要包含 WHERE 字段 + ORDER BY 字段 + 主键(用于覆盖)
- 子查询的
LIMIT仍存在深度偏移问题,但只作用于轻量级主键列,代价小很多 - 注意 MySQL 8.0+ 对这种写法优化更好,5.7 下需确认执行计划是否真用了索引
哪些索引设计会让分页失效
常见“假索引”陷阱:
-
INDEX(created_at)单独存在,但查询带WHERE user_id = 123→ 无法同时满足过滤与排序,要么走user_id索引然后filesort,要么走created_at索引但全表扫user_id -
INDEX(user_id, created_at)有,但排序是ORDER BY created_at DESC, id DESC→id不在索引中,仍需回表+额外排序 - 使用函数或表达式排序,如
ORDER BY DATE(created_at)→ 索引失效,必然filesort
真正有效的索引得匹配整个查询模式:WHERE 条件最左前缀 + ORDER BY 字段顺序 + 包含主键或所需查询列(覆盖)。
深度分页不是加个索引就能解决的事,关键在把“跳过 N 行”的思维,换成“从某条记录往后取”。游标分页看着麻烦,却是唯一能线性扩展的方式。











