深分页(limit 大 offset)天然低效,因需扫描并丢弃前 offset 行;高性能分页唯一可行方案是绕开 offset,采用基于排序字段值的游标分页(如 where id > 123456 order by id limit 20)。

深分页(LIMIT 带大 offset)在 MySQL 中天然低效,不是写法不对,而是执行机制决定的——它必须扫描并丢弃前 offset 行。想靠加索引或调参“救活” LIMIT 1000000, 20,基本是徒劳。真正可行的高性能分页,只有一条路:绕开 offset。
为什么 LIMIT offset, count 越往后越慢
MySQL 不会跳过前 offset 行,而是老老实实从排序结果开头读取 offset + count 行,再扔掉前 offset 行。哪怕你只想要 20 条,它也得把前 100 万行全读进内存、排序、再丢弃。
- 扫描行数 =
offset + count,和offset线性正相关 - 如果
ORDER BY字段没走覆盖索引,还会触发大量回表 - 即使有索引,B+ 树也得逐页遍历叶子节点,无法直接定位第 N 条
-
EXPLAIN里的rows值会明显飙升,但Extra字段往往只显示Using filesort或Using index,掩盖了真实代价
用 WHERE + 排序字段替代 OFFSET(游标分页)
核心思路:不记“第几页”,而记“上一页最后一条的值”。下一页查询直接从这个值之后开始,数据库能用索引快速定位,跳过所有中间行。
- 要求排序字段(如
id或created_at)有索引,且尽量唯一(避免重复值导致漏数据) - 升序分页:
SELECT * FROM orders WHERE id > 123456 ORDER BY id LIMIT 20 - 降序分页:
SELECT * FROM orders WHERE created_at - 复合排序(如
ORDER BY status, id)需用元组比较:WHERE (status, id) > (1, 99999),注意括号和字段顺序必须一致 - 客户端必须保存上一页最后一条的完整排序键,不能只存
id而忽略status
什么时候不能用游标分页?换方案前先看约束
游标分页不是银弹,它牺牲了“跳转任意页”的能力,也对数据稳定性有隐含要求。
- 不支持直接跳转第 50 页——只能顺序翻页(上一页 / 下一页),适合无限滚动或日志流场景
- 若排序字段非严格唯一(如多个记录同
created_at),可能漏掉或重复某几条;此时必须加入辅助唯一字段(如主键)构成联合条件 - 分页过程中若发生插入/删除,会导致“幻读”:用户可能看到新插入的记录,或某条记录在两页中都出现(取决于事务隔离级别)
- 如果业务强依赖页码(如 SEO 友好 URL
/page/50),得搭配缓存层做映射,或接受小数据量时仍用OFFSET
小数据量或必须跳页时的折中处理
当总数据量可控(比如 OFFSET 并非完全不可用,但要守住底线:
- 务必确保
ORDER BY字段有高效索引,且查询条件能命中该索引(避免全表扫描后再排序) - 限制最大可访问页码,例如前端禁用 > 第 200 页的跳转,后端校验
offset - 对高频分页接口加缓存(如 Redis 存
page_37的结果),但注意缓存失效策略要匹配数据更新频率 - 避免在
UPDATE或DELETE中用LIMIT做分批操作——这类语句本身不带ORDER BY,行为不可控;应显式加WHERE id BETWEEN ? AND ?分段
真正难的不是写出 LIMIT,而是判断什么时候不该用它。游标分页的“不灵活”,恰恰是它高性能的来源;而所有试图保留页码又想兼顾性能的方案,最终都会在一致性、延迟或实现复杂度上付出代价。











