数据量很大时,如何解决深分页问题?

考查重点:排序定位与深扫描成本。稳定排序用唯一键配游标向后取,减少 OFFSET 跳过;必须跳页可加锚点,索引覆盖减少回表

本题目录

参考答案

核心回答

MySQL 深分页变慢,主要是 LIMIT offset, size 需要产生并跳过大量前置结果;如果还伴随排序、非覆盖索引回表或复杂过滤,开销会进一步放大。有索引不等于能直接跳到“第十万条符合条件的记录”。

我会先看实际 SQL、过滤与排序、执行计划及扫描行数,区分偏移扫描、回表、排序和关联开销,再按业务交互选择方案:

  • 连续翻页或加载更多:优先使用基于上一页排序键的游标分页,配合匹配过滤与排序的索引。
  • 必须保留页码跳转:可以用覆盖索引先定位本页 ID,再延迟关联取完整行,但偏移扫描仍然存在。
  • 任意深页和大批量导出:结合筛选范围、分页锚点、受控结果集缓存或异步导出设计,明确新鲜度与一致性要求。

优化后既要比较耗时和扫描开销,也要验证排序稳定、过滤一致、边界不重不漏;不能只把 SQL 改短或看到命中索引就认定问题解决。

为什么越往后翻越慢

例如:

SELECT id, created_at, total_amount
FROM orders
WHERE tenant_id = 7 AND status = 'PAID'
ORDER BY created_at DESC, id DESC
LIMIT 100000, 20;

数据库需要先得到前面的匹配结果,丢弃 100000 条,再返回 20 条。在有足够结果时,这是至少 100020 条逻辑结果的处理;实际扫描量还取决于过滤、访问路径和排序。

如果能够沿索引顺序读取,可以减少额外排序,但仍须越过前面的结果。若取完整行必须回表,宽行读取又会增加成本;如果索引无法满足排序,则可能先处理更多候选再排序。具体执行方式应以当前版本、数据分布和执行计划为准。

接到线上优化任务时,先比较浅页与深页的同条件 SQL,观察实际索引、预计及实际处理行数、排序、回表、耗时和等待。EXPLAIN ANALYZE 会真正执行查询,应在可控负载和环境中使用,不能把它当作纯粹查看静态计划的命令。

连续翻页:按上一页的排序键继续查

假设 id 是全表唯一主键,created_at 非空,列表按创建时间倒序,同一时间再按 ID 倒序。可以考虑以下索引:

CREATE INDEX idx_orders_page
ON orders (tenant_id, status, created_at DESC, id DESC);

前两个字段服务于等值过滤,后两个字段对应列表顺序。是否采用此索引及访问范围,仍要结合附加条件、数据分布和计划验证。

第一页不带游标,按相同条件和排序取一页。假设上一页最后一条的 (created_at, id) 是 ('2026-10-15 12:00:00.000000', 801),下一页可以写成:

SELECT id, created_at, total_amount
FROM orders
WHERE tenant_id = 7
  AND status = 'PAID'
  AND (
    created_at < '2026-10-15 12:00:00.000000'
    OR (
      created_at = '2026-10-15 12:00:00.000000'
      AND id < 801
    )
  )
ORDER BY created_at DESC, id DESC
LIMIT 20;

这些值是说明游标语义的例子,应用应使用参数绑定。游标保存的是上一页的排序边界,不是“上一页 ID 加上页大小”,因此不依赖 ID 连续。

时间可能重复,所以必须同时保存 ID;只写 created_at < last_time,会漏掉与上一页末尾同一时间、但尚未读取的记录。升序时比较方向相反,混合升降序时则要逐字段匹配排序规则;可空字段还需要额外定义空值顺序与游标条件。

在索引支持范围与排序、过滤适配时,这种方式可以从边界附近继续读取,避免每页重新跳过越来越多的前置结果。它适合前后连续翻页,不天然支持直接跳到任意页码。需要上一页时,可以保留边界历史,或反向查询后再恢复展示顺序。

必须跳页:先定位本页 ID,再取完整行

对于需要兼容页码的宽表,可以尝试延迟关联:

SELECT o.id, o.created_at, o.total_amount
FROM orders AS o
JOIN (
  SELECT id, created_at
  FROM orders
  WHERE tenant_id = 7 AND status = 'PAID'
  ORDER BY created_at DESC, id DESC
  LIMIT 100000, 20
) AS p ON p.id = o.id
ORDER BY p.created_at DESC, p.id DESC;

内层在能够覆盖过滤与排序字段的索引上完成分页,只输出本页的 ID 和排序键;外层再按主键取得完整字段。它主要减少无效回表和宽行读取,不会把跳过 100000 条索引记录的工作变成零。

外层必须显式保持排序,不能假定 JOIN 自动保留子查询的顺序。过滤也应在定位页内完成,不能先取无过滤的一页 ID,再在外层过滤,否则可能改变分页语义或得到不足一页的结果。

MySQL 对派生表有合并与物化策略,包含 LIMIT 会阻止相应的合并;这类写法仍可能有临时表和关联成本。是否更快应看真实计划,不能概括为“INNER JOIN 一定没有临时表”或“深分页已经变成只扫描一页”。若原查询本身已是覆盖索引且没有明显回表收益,增加关联甚至可能不划算。

任意深页和数据变化还要处理什么

页码跳转的代价不能凭空消失

如果用户必须随机访问很深的页,可以先让业务缩小时间、状态等范围;反复查询相同大结果集时,也可以考虑保存有序 ID 集合或分页锚点,查询时从较近的边界继续。

这些方案需要维护查询条件、排序、权限、版本和有效期,并承担预计算与存储成本。结果集缓存还会带来新鲜度取舍。部分数据仓库提供专有分页缓存,例如 AnalyticDB MySQL 版的 Paging Cache;这不是标准 MySQL 自动具有的能力,不能直接套用其配置。

总数查询也要单独评估。若产品只需要“还有没有下一页”,可以多取一条判断;不必每次深分页都同时计算完整、精确的总数。

游标稳定不等于数据快照

在排序键和过滤条件稳定的情况下,游标不会因为前面一条记录被删除,就像 OFFSET 那样发生位置平移。新插入到游标之前的数据,通常留到刷新列表时展示。

但如果排序字段、过滤状态或记录本身持续变化,记录仍可能跨过边界,出现当前遍历看不到或重复看到的情况。唯一排序键只保证同一数据视图里的顺序,不保证多次请求天然共享快照。

需要全量导出时,应明确固定快照或物化结果集等语义;跨很长时间维持数据库事务也有资源和历史版本保留成本。分页游标应绑定过滤与排序条件,必要时校验完整性,不能修改游标就改变租户或访问范围。

如何验证优化有效

在相同过滤、排序和可比较的数据条件下,对比浅页与深页的执行计划、处理行数、回表、排序、耗时与数据库资源。分别验证:

  • 多条记录时间相同、ID 有间隙时,连续翻页顺序仍然正确。
  • 租户、状态等过滤在每页都一致,分页结束返回空结果。
  • 延迟关联返回的 ID、顺序和原查询相同。
  • 插入、删除和排序字段变化时,行为符合事先约定的实时或快照语义。

常见追问

只加一个索引能解决吗?

索引可能减少排序、过滤和回表开销,但 OFFSET 仍要跳过前置匹配记录。连续翻页时,改变访问方式通常比只给深 OFFSET 增加索引更直接。

游标为什么不能直接用 ID?

只有实际按 ID 排序时,单个 ID 才是合适边界。按时间、分数等字段排序,需要对应排序键和唯一 ID 一起构造游标;否则分页顺序可能改变。

是不是该直接换 Elasticsearch?

先判断是列表查询还是复杂搜索,以及页码和一致性需求。搜索引擎也有深分页成本及其游标机制,迁移不能自动消除问题,还会增加同步和运维工作。

参考资料

  • 国内:JavaGuide:深度分页介绍及优化建议:借鉴范围查询、覆盖索引和延迟关联的讨论,区分正文中减少偏移扫描与减少宽行回表的优化目标;并发和物化行为仍按数据库机制限定。
  • 国内:JavaGuide:MySQL 执行计划分析:参考索引、访问路径及估算和实际执行统计,解释正文中怎样定位深分页的具体开销,并验证改写后的执行计划。
  • 国内:阿里云:Paging Cache:借鉴保存结果集复用分页及缓存有效期、清理的机制,说明正文中物化有序结果支持随机深页的思路及资源、新鲜度代价;所述功能属于 AnalyticDB MySQL 版。
  • 国外:MySQL 8.4:LIMIT Query Optimization:依据 LIMIT 的排序选择和同值顺序不确定的说明,解释正文中分页排序需要增加唯一 ID 的原因。
  • 国外:MySQL 8.4:ORDER BY Optimization:参考等值前缀、联合索引、升降序和额外排序的条件,说明正文中索引怎样匹配过滤条件与游标顺序。

相关面经

5 篇

字节跳动

Agent 开发
校招秋招一面、二面、三面
查看原面经 →
使用 `LIMIT offset, size` 扫描百万或上亿行数据有什么性能问题?
追问
怎样使用游标分页优化深分页?游标为什么要包含唯一键?

阿里巴巴

AI 应用研发工程师
校招秋招一面
查看原面经 →
MySQL深分页为什么越往后越慢?怎么优化

去哪儿

AI 全栈开发工程师
查看原面经 →
对于数据量很高的情况下,深分页问题怎么解决?

虾皮

秋招一面
查看原面经 →
MySQL深分页优化有了解吗

蚂蚁国际

AI 应用 / Agent 开发
暑期实习一面
查看原面经 →
你平时用的数据库是MySQL吧?如果线上数据量特别大,分页查询变慢,拿到这个优化任务,你的操作步骤是怎样的?
追问
如果单纯是数据量大导致深度分页查询慢,优化思路是什么?

相关题目

4 题