MySQL 深分页为什么慢,OFFSET 和游标分页怎么选

3次阅读
没有评论

MySQL 分页查询刚开始通常都很快,写法也很自然:

SELECT * FROM post ORDER BY id DESC LIMIT 20 OFFSET 0;

但页码一深,问题就来了。比如后台列表跳到第几万页,或者接口里出现:

SELECT * FROM test WHERE val = 4 LIMIT 300000, 5;

这类 SQL 即使字段上有索引,也可能变慢。原因不是“索引没用”,而是 OFFSET 太大时,数据库必须先走过大量用不到的行,再把它们丢掉。

OFFSET 的成本不是从结果开始算

LIMIT 300000, 5 的直觉很容易被误解。它不是直接定位到第 300001 条,然后读 5 条就结束。更接近的理解是:

  1. 按查询条件和排序规则找到扫描起点。
  2. 沿着索引顺序往后读。
  3. 先跳过前 300000 条。
  4. 再返回后面的 5 条。

前面被跳过的 300000 条虽然不会出现在结果里,但它们对应的索引扫描成本已经发生了。

所以深分页慢的核心不是“表里有多少行”,而是“这次查询为了到达目标页,要路过多少行”。

SELECT * 会放大回表成本

如果查询只需要索引里的字段,MySQL 可以少读很多数据。但如果写的是 SELECT *,情况就变重了。

InnoDB 的二级索引叶子节点里通常保存的是索引列和主键值。通过二级索引找到候选记录后,如果还要拿整行数据,就要按主键回到聚簇索引里再读一次,这就是回表。

深分页时,很多被回表读取的行最后会被 OFFSET 丢掉。也就是说,数据库做了大量“读出来再扔掉”的工作。

这也是为什么同样是分页,下面两类查询体感差很多:

-- 更容易慢:宽行 + 大 OFFSET
SELECT * FROM test WHERE val = 4 LIMIT 300000, 5;

-- 成本相对小:只查索引列或主键
SELECT id FROM test WHERE val = 4 LIMIT 300000, 5;

索引仍然有价值,但它不能把大 OFFSET 变成零成本。

延迟关联可以减少无效回表

如果业务暂时必须保留页码分页,可以先做一个折中:先只查主键,再回表取目标页的完整数据。

示意写法:

SELECT t.*
FROM test t
JOIN (
  SELECT id
  FROM test
  WHERE val = 4
  ORDER BY id
  LIMIT 300000, 5
) page ON page.id = t.id;

这样做的思路是:深分页那一步尽量只在索引上走,先拿到最终需要的 5 个 id,再对这 5 行回表取完整字段。

它不能消灭大 OFFSET 的索引扫描成本,但可以减少 SELECT * 带来的大量无效回表。对于宽表、字段多、行数据大的场景,收益会更明显。

不过这不是万能方案。如果 OFFSET 非常大,索引扫描本身仍然会慢。

游标分页从上一页书签继续读

如果业务不要求“任意跳到第 8473 页”,更推荐用游标分页,也常叫 keyset pagination。

游标分页的核心是:客户端带上上一页最后一条记录的排序键,下一页从这个位置继续读。

例如按 created_atid 倒序:

-- 第一页
SELECT *
FROM post
WHERE category_id = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 下一页:上一页最后一条是 created_at = '2026-06-10 10:00:00', id = 10086
SELECT *
FROM post
WHERE category_id = 1
  AND (created_at, id) < ('2026-06-10 10:00:00', 10086)
ORDER BY created_at DESC, id DESC
LIMIT 20;

这类查询的好处是,它不需要每次都从第一页开始数到第 N 页,而是直接从上次结束的位置附近继续读。只要索引设计匹配,越往后翻页,单页成本也能保持相对稳定。

页码分页和游标分页怎么选

可以按业务交互方式来判断:

| 场景 | 更适合的方式 |

| — | — |

| 后台管理页,需要跳页、查第几页 | 页码分页,可以配合限制最大页数和延迟关联 |

| 信息流、时间线、消息列表、日志列表 | 游标分页 |

| 导出大量数据 | 不要用普通分页接口硬翻,改成异步导出或批任务 |

| 搜索结果只看前几页 | 页码分页可以接受,但限制深页访问 |

| 数据变化频繁,要求翻页稳定 | 游标分页更稳 |

页码分页的优点是用户理解简单,可以显示总页数和跳页。缺点是越深越贵,而且数据变化时可能出现重复或漏数据。

游标分页的优点是性能稳定,适合连续向后翻。缺点是不适合任意跳页,也不天然提供“总共有多少页”。

索引要和筛选和排序一起设计

无论使用哪种分页方式,索引都要和 WHEREORDER BY 一起看。

比如下面这个游标查询:

SELECT *
FROM post
WHERE category_id = 1
  AND (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 20;

可以考虑围绕 category_idcreated_atid 设计联合索引。否则即使用了游标条件,排序和过滤也可能变成额外成本。

实际落地时不要只凭感觉改索引,至少要看:

  • EXPLAIN 的访问类型和使用索引。
  • 是否出现 filesort。
  • 回表行数是否过大。
  • 慢查询里的扫描行数和返回行数差距。

小结

MySQL 深分页慢,根源通常不是“没加索引”这么简单,而是大 OFFSET 要求数据库先路过大量记录,再丢掉它们。

实用处理顺序可以这样记:

  1. 小页码分页:普通 LIMIT offset, size 可以接受。
  2. 深页码但必须保留跳页:考虑限制最大页数、只查主键再延迟关联。
  3. 连续翻页或信息流:优先改成游标分页。
  4. 大批量导出:不要伪装成分页接口,做异步导出或批处理。

分页优化的重点不是把 SQL 写得更花,而是先分清业务到底需要“跳到某一页”,还是只需要“从上一页继续看”。这个问题分清了,后面的索引和 SQL 才有方向。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-06-10发表,共计2328字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)