翻到第 10 万页时数据库在做什么:深分页的代价与三种替代方案

举报
yd_222021590 发表于 2026/11/08 21:00:29 2026/11/08
【摘要】 LIMIT 20 OFFSET 40000 为什么越翻越慢,以及游标分页、延迟关联、限制翻页深度该怎么选。

翻到第 10 万页时数据库在做什么:深分页的代价与三种替代方案

一个后台列表页,前几页秒开,产品经理往第 2000 页一点,页面转圈转到超时。查日志发现那条 SQL 本身没写错,索引也在,就是慢——慢在 LIMIT 20 OFFSET 40000 这一句上。

这类问题在数据量涨起来之后几乎必然出现,而且报障的描述往往是"列表偶尔很卡",很容易被当成网络或前端的问题,排查方向一开始就跑偏。

一、OFFSET 的代价:跳过的行也要读

很多人以为 OFFSET 40000 是"直接跳到第 40000 行",数据库省了力气。恰恰相反:它必须先把前 40000 行找出来、再丢掉。

执行过程大致是:沿着索引顺序扫描,每读到一行就判断"够不够 offset",不够就丢,够了才开始返回。所以 offset 越大,扫描的行数越多,成本是线性增长的。第 1 页扫 20 行,第 2000 页要扫 40020 行,第 10 万页要扫 200 万行。

如果查询里还有 ORDER BY、多表 JOIN 或者 SELECT *,这 40000 行还得每行去回表取数据,代价会成倍放大。这就是为什么"索引建了还是慢"——索引确实用上了,问题是它被迫扫了太多行。

二、EXPLAIN 里能看到什么

看 rows 这一列,深分页时它通常等于 offset + limit,而不是 limit。这一条基本就能定位。

另一个信号是 Using filesort:如果排序字段没走索引,数据库要把全部候选行取出来排序,再丢掉前面几万行,等于把最贵的那步做了两遍。

三、方案一:游标分页(keyset pagination)

核心思路是别用"第几行"定位,用"上一页最后一条的值"定位:

-- 传统写法:越翻越慢
SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 40000;

-- 游标写法:恒定成本
SELECT id, title, created_at
FROM articles
WHERE id < 40000          -- 上一页最后一条的 id
ORDER BY id DESC
LIMIT 20;

第二种写法里,WHERE id < ? 直接把索引定位到起点,扫 20 行就停,和翻到第几页无关。数据量再大也是这个成本。

它的代价是不能跳页:你只能"下一页/上一页",没法直接点第 500 页。对信息流、消息列表、导出这类场景完全够用;对"必须有页码导航"的后台就不行。

四、方案二:延迟关联

如果非要用 offset,至少别让那 40000 行都去回表:

SELECT a.id, a.title, a.created_at
FROM articles a
JOIN (
    SELECT id FROM articles
    ORDER BY id DESC
    LIMIT 20 OFFSET 40000
) t ON t.id = a.id;

子查询只在索引上扫、只取主键,不回表;拿到 20 个 id 之后再回表取详情。深分页的瓶颈通常就在回表,这一改往往能快一个数量级——但注意,它只是把成本降低了,没有改变线性增长的本质,翻得足够深还是会慢。

五、方案三:换一种"页"的定义

后台列表如果一定要有页码,可以不做真正的 offset 分页,而是:

  • 限制最大可翻页数:只允许翻到前 N 页,更深的查询强制加筛选条件(时间范围、状态)。这是最简单的止损,且业务上几乎总能接受。
  • 按业务主键分段:比如按月份、按租户分桶,先选桶再在桶内分页,offset 永远很小。
  • 预计算序号:给每行维护一个稳定的递增序号,翻页变成 WHERE seq BETWEEN ? AND ?,代价恒定。

这三种都属于"改需求"而不是"改 SQL",但往往比硬优化 SQL 划算得多。

六、还有一个容易忽略的点:总数

SELECT COUNT(*) 往往和深分页一起出现,而且可能比列表本身还慢。如果页码只用于"上一页/下一页",很多场景根本不需要总数;如果需要,也可以缓存或做近似值(比如"约 1.2 万条"),没必要每次精确到个位。

小结

  • OFFSET n 不是跳转,是先扫 n 行再丢掉,成本随 n 线性增长。
  • 深分页慢的根因是"扫描 + 回表",不是索引没建。
  • 游标分页(WHERE id < last_id)成本恒定,代价是不能跳页。
  • 延迟关联能显著降低回表成本,但不改变线性本质。
  • 实在要页码,优先考虑"限制翻页深度"“按主键分段”“预计算序号”。
  • 顺手检查 COUNT(*),它经常是隐藏的慢查询。

判断标准其实很简单:如果一个列表,用户根本不会翻到第 1000 页,那就别让数据库为第 1000 页做准备。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。