MySQL中OFFSET 越大越慢怎么解决

 更新时间:2026年06月21日 09:26:22   作者:花生了什么事o  
本文主要介绍了深分页问题优化策略,探讨LIMIT OFFSET分页性能瓶颈,提出延迟关联、游标分页及子查询优化三种方案,帮助解决大OFFSET值下性能下降问题

深分页问题

一个商品列表页,后端接口用的分页查询:

SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 0;

前几页加载很快,用户也没啥感觉。但当翻到第 500 页的时候,接口响应时间从 50ms 飙到了 3 秒。你打开慢查询日志一看,又是这条 SQL 在搞事。

这就是深分页问题,表里有 50 万条数据,id 是主键,按理说走索引应该很快。但 OFFSET 一大,性能就断崖式下跌。这不是个例,几乎所有用 LIMIT offset, count 做分页的系统,随着数据量的增加都会撞上这堵墙。

LIMIT offset, count 到底在干什么

先看一条最简单的分页 SQL:

SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 1000;

这条语句的执行过程是这样的:

1. MySQL 从索引(主键)上从第一条开始,逐条往后扫
2. 扫到第 1 条时开始计数,跳过前 1000 条
3. 从第 1001 条开始,取 20 条返回
4. 对这 20 条记录,回表取完整行数据

关键在第 2 步。MySQL 必须逐条跳过前 1000 条记录,即使它不需要这些数据。 这些被跳过的记录,MySQL 一样要扫描、一样要比较,只是最终不返回而已。

跳过不等于不扫描。OFFSET 越大,跳过越多,扫描越多。

为什么 OFFSET 越大越慢

用 EXPLAIN 看一下这条查询的执行计划:

EXPLAIN SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 1000;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | products | NULL       | index| NULL          | PRIMARY | 8     | NULL | 1020 | 100.00   | NULL  |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+

注意 type = indexrows = 1020。type 为 index 说明走了全索引扫描(遍历整棵索引树),rows 为 1020 说明预估要扫描 1020 行。

OFFSET 越大,这个 rows 值就越大。扫到第 100 万页时,光跳过就得扫描 100 万条记录。即使每条记录扫描只要 0.1 毫秒,100 万条也要 100 秒。

更糟的是,这个查询除了扫描索引,还要回表取 * 的所有字段。每一条被跳过的记录,MySQL 可能都要做一次回表。 因为 SELECT * 取的是完整行数据,索引里存不下了,必须回表。

这就是深分页慢的两个根源:

  1. 扫描浪费:OFFSET 越大,MySQL 丢弃的记录越多,但扫描成本不变
  2. 回表浪费SELECT * 导致每条被跳过的记录都可能触发回表

方案一:延迟关联,先查 ID 再取数据

延迟关联的核心思路是:先用覆盖索引快速拿到需要的 ID,再用 ID 回表取完整数据。

SELECT p.* FROM products p
INNER JOIN (
    SELECT id FROM products ORDER BY id LIMIT 20 OFFSET 1000
) t ON p.id = t.id;

这条 SQL 分两步执行:

第一步(子查询):
    SELECT id FROM products ORDER BY id LIMIT 20 OFFSET 1000
    → 只扫主键索引,不需要回表,快速拿到 20 个 ID

第二步(外层查询):
    SELECT p.* FROM products p WHERE p.id IN (...)
    → 用主键精确查 20 条,直接走聚簇索引,零回表

为什么这样更快?对比一下:

步骤原始写法延迟关联
扫描阶段扫描 1020 条,每条都要判断扫描 1020 条,只读 ID(覆盖索引)
回表阶段跳过的 1000 条也可能回表跳过的 1000 条不回表
取数阶段20 条全量回表20 条精确回表

子查询用了覆盖索引(只取 id),扫描阶段的开销大幅降低。外层查询用主键精确查找,不用扫描、不用排序。

方案二:游标分页,用上一页的最后一条当起点

延迟关联解决了回表浪费,但扫描浪费还在——OFFSET 1000 时还是要跳过 1000 条。游标分页直接把 OFFSET 干掉了。

思路是:记住上一页最后一条记录的 ID,下一页查询时从这个 ID 之后开始取。

-- 第一页
SELECT * FROM products ORDER BY id LIMIT 20;
-- 返回的最后一条 id = 1000

-- 第二页:从 id = 1000 之后开始
SELECT * FROM products WHERE id > 1000 ORDER BY id LIMIT 20;

-- 第三页:从上一页最后一条 id = 1020 之后开始
SELECT * FROM products WHERE id > 1020 ORDER BY id LIMIT 20;

EXPLAIN 看一下执行计划:

EXPLAIN SELECT * FROM products WHERE id > 1000 ORDER BY id LIMIT 20;
+----+-------------+----------+------------+-------+---------------+---------+---------+------+------+----------+-------+
| id | select_type | table    | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+------+----------+-------+
|  1 | SIMPLE      | products | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |  20  | 100.00   | NULL  |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+------+----------+-------+

type = rangerows = 20。MySQL 直接定位到 id > 1000 的位置,取 20 条就停了。不管翻到第几页,扫描行数永远是 20。

但游标分页有局限:只能"下一页",不能跳页。 用户点第 5 页,你没法直接算出对应的 ID 是多少。所以它适用于无限滚动、加载更多这类场景,不适合有页码的分页器。

方案三:子查询优化,让 MySQL 先走索引

这个方案适合没有主键可用、或者排序字段不是主键的场景。

SELECT p.* FROM products p
WHERE p.id >= (
    SELECT id FROM products ORDER BY id LIMIT 1 OFFSET 1000
)
ORDER BY p.id
LIMIT 20;

子查询只执行一次,拿到 OFFSET 位置的那条记录的 ID。外层查询从这个 ID 开始往后取 20 条。

和延迟关联的区别在于:延迟关联是"先查一批 ID,再用 ID 取数据";这个方案是"先找一个起点 ID,再从起点往后取"。子查询只返回一条记录,开销极小。

用伪代码理解:

// 子查询:找起点
start_id = SELECT id FROM products ORDER BY id LIMIT 1 OFFSET 1000

// 外层:从起点取数据
SELECT * FROM products WHERE id >= start_id ORDER BY id LIMIT 20

外层查询 id >= start_id 加上 ORDER BY idLIMIT 20,MySQL 可以直接走主键范围扫描,rows 只有 20。

三种方案对比

方案原理适用场景能否跳页性能
延迟关联覆盖索引查 ID,再回表取数据通用,改造成本低OFFSET 大时显著提升
游标分页用上一页 ID 当起点,去掉 OFFSET无限滚动、加载更多不能任何 OFFSET 下恒定
子查询优化子查询找起点,外层范围取数排序字段不是主键时子查询开销小,外层走范围

选择建议:

  • 有页码导航的需求(后台管理系统、商品搜索):延迟关联或子查询优化
  • 无限滚动、信息流(朋友圈、微博):游标分页
  • 数据量千万级:游标分页是唯一选择,其他方案在超大 OFFSET 下依然会退化

小结

深分页慢的根源:OFFSET 越大,MySQL 丢弃的数据越多,但扫描的成本一点没少。 延迟关联用覆盖索引减少了回表浪费,子查询优化用一个精确的起点取代了逐条跳过,游标分页则直接绕过了 OFFSET 的问题。三者本质都在做同一件事:让 MySQL 跳过那些不需要的记录,而不是扫描了再丢掉。

到此这篇关于MySQL中OFFSET 越大越慢怎么解决的文章就介绍到这了,更多相关MySQL OFFSET 越大越慢内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • mysql删除语句超详细汇总

    mysql删除语句超详细汇总

    这篇文章主要给大家介绍了关于mysql删除语句超详细汇总的相关资料,SQL是用于访问和处理数据库的标准的计算机语言,简称结构化查询语言,SQL中的删除语句有多种方法,这里总结下,需要的朋友可以参考下
    2023-08-08
  • MySQL如何修改binlog保存的天数

    MySQL如何修改binlog保存的天数

    本文介绍了如何修改MySQL的binlog保存天数为7天,设置了不会立即清除,需触发特定条件,同时提到purge命令用于清除指定binlog,并举例说明
    2026-04-04
  • mysql 8.0.17 winx64(附加navicat)手动配置版安装教程图解

    mysql 8.0.17 winx64(附加navicat)手动配置版安装教程图解

    这篇文章主要介绍了mysql 8.0.17 winx64(附加navicat)手动配置版安装教程图解,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • 如何使用mysqladmin获取一个mysql实例当前的TPS和QPS

    如何使用mysqladmin获取一个mysql实例当前的TPS和QPS

    这篇文章主要介绍了如何使用mysqladmin这个工具来获取一个mysql实例当前的TPS和QPS,帮助大家更好的管理数据库,感兴趣的朋友可以了解下
    2020-11-11
  • SQL查询超时的设置方法(关于timeout的处理)

    SQL查询超时的设置方法(关于timeout的处理)

    为了优化OceanBase的query timeout设置方式,特调研MySQL关于timeout的处理,下面与大家分享下处理记录,感兴趣的朋友可以参考下哈
    2013-04-04
  • MySQL 横向衍生表(Lateral Derived Tables)的实现

    MySQL 横向衍生表(Lateral Derived Tables)的实现

    横向衍生表适用于在需要通过子查询获取中间结果集的场景,相对于普通衍生表,横向衍生表可以引用在其之前出现过的表名,本文就来介绍一下MySQL 横向衍生表(Lateral Derived Tables)的实现,感兴趣的可以了解一下
    2025-06-06
  • MySql中 is Null段判断无效和IFNULL()失效的解决方案

    MySql中 is Null段判断无效和IFNULL()失效的解决方案

    这篇文章主要介绍了MySql中 is Null段判断无效和IFNULL()失效的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • 解读MySQL为什么不推荐使用外键

    解读MySQL为什么不推荐使用外键

    这篇文章主要介绍了解读MySQL为什么不推荐使用外键问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04
  • 深入MySQL调优原则

    深入MySQL调优原则

    MySQL的调优是为了确保数据库在高负载和大数据量情况下能够高效稳定运行,调优原则主要包括硬件调优、系统配置调优、MySQL配置调优、模式设计调优、查询优化等,感兴趣的可以了解一下
    2025-08-08
  • MySQL 表新增字段时报丢失连接错误

    MySQL 表新增字段时报丢失连接错误

    MySQL在新增字段时遇到"Lost connection to MySQL server during query"错误,可能由于网络问题、查询超时或内存不足等原因,下面就来详细的介绍一下该问题的解决,感兴趣的可以了解一下
    2026-01-01

最新评论