【MySQL篇|第十篇】深分页问题

一、什么是深分页

深分页(Deep Pagination)指的是在分页查询中,页码非常大、偏移量非常靠后的场景。例如用户翻到第 10000 页,每页 20 条,那么数据库需要跳过前 199980 条记录,再返回后面的 20 条。

以常见的 LIMIT offset, size 写法为例:

sql 复制代码
SELECT id, title, created_at
FROM article
ORDER BY created_at DESC
LIMIT 199980, 20;

这条 SQL 看起来很简单,但执行代价并不小。数据库通常需要先按照排序条件扫描并丢弃前 199980 行,然后才能取到目标 20 行。偏移量越大,扫描和丢弃的成本越高,查询耗时也就越长。浪费了很多cpu的性能才查找到需要的数据,前面做的都是无效查询。

二、深分页为什么会慢

深分页的性能问题主要来自以下几个方面:

  • 大量无效扫描 :offset 之前的数据虽然不返回,但仍需要被读取、排序或过滤,属于纯浪费的 I/O 和 CPU 开销。
  • 回表次数多:如果查询字段不在索引中,每一行都需要回表取数据,偏移量越大回表次数越多。
  • 排序成本高:当排序字段没有合适索引时,数据库可能需要对大量数据进行文件排序(filesort),内存不足时还会落盘。
  • 结果不稳定:如果排序字段存在重复值且没有唯一键兜底,翻页过程中可能出现数据重复或遗漏。

一句话总结:深分页慢的本质是「为了拿到少量数据,却付出了扫描大量数据的代价」。

三、常见的解决方案

3.1 基于游标(Keyset)分页

游标分页也叫「书签分页」或「Seek 方法」,核心思路是记住上一页最后一条记录的位置,下一页从该位置之后继续取,而不是从头跳过 offset 条。

sql 复制代码
-- 第一页
SELECT id, title, created_at
FROM article
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 后续页:以上一页最后一条的 created_at 和 id 作为游标
SELECT id, title, created_at
FROM article
WHERE (created_at, id) < ('2026-10-01 12:00:00', 10086)
ORDER BY created_at DESC, id DESC
LIMIT 20;

这种方式的优势是每次查询都能利用索引直接定位到起始位置,扫描行数基本恒定,与页码无关。需要注意的是,它只支持「上一页/下一页」式的顺序翻页,无法直接跳到任意页码。

3.2 延迟关联(Deferred Join)

如果业务必须支持跳页,可以先只在索引上完成分页,拿到主键后再回表取完整数据,减少回表次数。

sql 复制代码
SELECT a.id, a.title, a.created_at
FROM article a
INNER JOIN (
    SELECT id
    FROM article
    ORDER BY created_at DESC
    LIMIT 199980, 20
) t ON a.id = t.id
ORDER BY a.created_at DESC;

子查询只扫描索引列,避免了对宽表的频繁回表,通常能带来明显的性能提升。

3.3 覆盖索引

让查询涉及的字段全部包含在索引中,使数据库可以直接从索引返回结果,无需回表。例如为 (created_at, id, title) 建立联合索引,查询时只取这三列即可。

覆盖索引往往与延迟关联配合使用,效果更好,但要注意索引本身也会占用存储并增加写入成本。

3.4 限制最大可翻页数

很多产品并不需要真正支持无限翻页。可以在产品层面限制最大页码,例如只允许查看前 100 页,超出后引导用户使用筛选条件缩小范围。这是成本最低、见效最快的方案之一。

3.5 使用搜索引擎或专用存储

对于搜索、日志、商品列表等场景,可以把查询交给 Elasticsearch、OpenSearch 等搜索引擎。它们通过 search_after 参数实现游标式分页,避免深度遍历。

json 复制代码
{
  "size": 20,
  "sort": [
    { "created_at": "desc" },
    { "id": "desc" }
  ],
  "search_after": ["2026-10-01T12:00:00", 10086]
}

需要注意,Elasticsearch 的 from + size 同样存在深分页限制,默认 max_result_window 为 10000,超过后应改用 search_after 或 scroll。

3.6 预计算与数据冗余

对于数据量相对固定、查询模式稳定的场景,可以提前计算好分页结果并缓存,或者维护一张带连续序号的辅助表,把 offset 查询转化为按序号的范围查询。

四、方案对比与选型建议

方案 是否支持跳页 性能表现 适用场景
游标分页 否 优秀,与页码无关 信息流、时间线、无限滚动
延迟关联 是 较好 后台管理列表、需要跳页
覆盖索引 是 较好 查询字段较少且固定
限制最大页数 部分 优秀 大多数 C 端列表
搜索引擎 视实现而定 优秀 搜索、日志、海量数据

实际选型时,建议先明确业务是否真的需要「跳到第 N 页」。如果不需要,优先选择游标分页;如果必须支持跳页,再考虑延迟关联加覆盖索引,并配合最大页数限制兜底。

五、实践中的注意事项

  • 排序字段要唯一:游标分页必须保证排序键唯一,通常用「业务字段 + 主键」组合,避免翻页时数据重复或丢失。
  • 索引要匹配排序 :ORDER BY 的字段顺序应与联合索引顺序一致,否则仍可能触发文件排序。
  • 避免 SELECT *:只查询必要字段,减少回表和网络传输开销。
  • 关注总数统计 :COUNT(*) 在大表上同样昂贵,可以考虑近似统计或缓存总数。
  • 做好压测:不同数据分布下执行计划可能差异很大,上线前应针对真实数据量做压测验证。

六、总结

深分页的本质是「大偏移量带来的无效扫描」。解决思路可以归纳为三类:一是用游标替代 offset,从根上消除偏移;二是通过延迟关联、覆盖索引降低单次查询成本;三是在产品层面限制翻页深度或引入搜索引擎。理解业务对「跳页」的真实需求,往往比单纯优化 SQL 更有效。

相关推荐
ClouGence2 小时前
SQL 效率天花板!开源 CloudDM,自带一站式 AI SQL 助手
数据库·sql·开源
Decay__2 小时前
附-01 · MySQL 压测与容量规划 —— sysbench 基准测试、容量评估与参数基线
运维·数据库·sql·mysql·架构
这个DBA有点耶2 小时前
主从切换报成功,业务却写不进去——数据层脑裂与应用层连接残留的两类假成功
数据库·架构·dba
qq_452396232 小时前
第八篇:《VACUUM、自动清理与膨胀治理》
jvm·数据库·oracle
SelectDB技术团队2 小时前
ClickHouse 支持存算分离吗?Doris 社区版原生存算分离怎么落地
数据库·clickhouse·云原生·开源·apache doris·技术选型·存算分层
DBA_G2 小时前
南大通用GBase 8a数据库可视化灾备解决工具GVR
数据库
JJJennie7773 小时前
MAI Gateway实战部署:大模型网关部署方案?从0到1上手指南
数据库·人工智能·gateway·ai网关
吹什么轩3 小时前
MySQL 内连接与外连接:从笛卡尔积到 LEFT JOIN,一篇讲透
数据库·mysql
涛思数据(TDengine)3 小时前
支撑药企全球化合规:TDengine 发布中美欧三地监管对照白皮书
大数据·数据库·人工智能·ai·tdengine