offset在百万数据下变慢是因为mysql需真实扫描并丢弃前n行,导致i/o和排序开销剧增;应改用游标分页(如where 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 」,跳过全表扫描。前提是排序字段有索引且唯一(如自增 <code>id或时间戳created_at)。示例(上一页最后一条
id = 5002341):SELECT * FROM articles WHERE id <p>注意点:</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站"><img src="https://img.php.cn/upload/skill/000/000/081/179040786932301.jpg" alt="btpanel phpsite 宝塔面板PHP网站" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站" class="overflowclass">btpanel phpsite 宝塔面板PHP网站</a> <p class="overflowclass">宝塔面板 PHP 网站管理:站点创建、删除、启停、PHP 版本切换、域名管理、SSL证书管理、伪静态管理、数据库管理</p> </div> <a rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div>
- 必须确保
ORDER BY字段和WHERE条件字段一致,否则索引可能失效- 如果是
ORDER BY created_at DESC, id DESC,则条件需写成WHERE (created_at, id) (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),且所有分页请求都得带上这个完整排序规则。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











