OFFSET在百万数据下变慢是因为MySQL需真实扫描并丢弃前N行,导致I/O和排序开销剧增;应改用游标分页(如WHERE id < 上一页末id)或预存锚点优化。

为什么 OFFSET 在百万数据下会越来越慢
因为 MySQL 的 OFFSET 不是跳过索引位置,而是真实扫描前 N 行再丢弃——当 OFFSET 1000000 时,它仍要读取并跳过 100 万行,哪怕你只要 20 条。磁盘 I/O 和临时排序开销会指数级上升,响应可能从几十毫秒变成几秒。
实操建议:
- 永远避免
SELECT * FROM table ORDER BY id DESC LIMIT 1000000, 20这类写法 - 确认执行计划:用
EXPLAIN SELECT ...看rows是否随OFFSET增大而线性增长 - 如果主键连续、无大量删除,优先改用「游标分页(cursor-based pagination)」
用主键范围查询替代 LIMIT OFFSET
核心思路是把「第 N 页」转换为「id < 上一页最后一条的 id」,跳过全表扫描。前提是排序字段有索引且唯一(如自增 id 或时间戳 created_at)。
示例(上一页最后一条 id = 5002341):
立即学习“PHP免费学习笔记(深入)”;
SELECT * FROM articles WHERE id < 5002341 ORDER BY id DESC LIMIT 20;
注意点:
- 必须确保
ORDER BY字段和WHERE条件字段一致,否则索引可能失效 - 如果是
ORDER BY created_at DESC, id DESC,则条件需写成WHERE (created_at, id) < ('2024-05-01', 5002341)(MySQL 8.0+ 支持行构造器) - 不能直接跳转任意页(比如“跳到第 500 页”),只适合「下一页/上一页」场景
如何支持「跳转指定页码」又不崩
真要保留页码输入(比如后台管理),就得绕开 OFFSET 的性能陷阱。常见做法是预计算或缓存边界值。
推荐方案:
- 对每 1000 条记录存一个锚点:
INSERT INTO page_anchors (page_num, min_id, max_id) VALUES (1000, 123456, 124455),查第 1000 页时先查锚点再范围查询 - 用
SELECT COUNT(*)获取总条数做页码栏显示,但不要用它算OFFSET;总条数可走近似值:SHOW TABLE STATUS LIKE 'articles'中的Rows字段(MyISAM 精确,InnoDB 是估算) - 用户输入页码后,先用
SELECT id FROM articles ORDER BY id DESC LIMIT $offset, 1只查主键,再用这个id做范围查询——比查全字段快一个数量级
PHP 函数里容易忽略的细节
翻页函数不是拼 SQL 就完事,边界和类型处理不到位,线上立刻报错或漏数据。
关键检查项:
-
$page和$per_page必须严格过滤:filter_var($page, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]),防止负数或非数字导致 SQL 注入或逻辑错乱 - MySQL 的
SQL_CALC_FOUND_ROWS已被弃用(8.0.17+ 移除),别再用;总条数改用单独SELECT COUNT(*)或前述估算方式 - 游标分页时,前端传来的
last_id必须校验存在且为整型,否则查不到结果也不该静默返回空数组,应明确抛出异常或返回提示 - 如果业务允许,把分页参数从
?page=500改成?cursor=5002341,避免页码语义误导(比如删了数据后页码对应内容已变)
最麻烦的其实是「排序字段不唯一」——比如按 status 分页,相同 status 的记录可能每次顺序不一致,导致重复或遗漏。这时候必须加二级排序(如 ORDER BY status, id),且所有分页请求都得带上这个完整排序规则。



















