告别“深分页”噩梦:亿级数据下的高性能分页查询实战指南

在系统上线初期,数据量只有几万条时,LIMIT 10, 20 这样的分页查询跑得飞快。然而,随着业务增长,当数据量突破千万甚至亿级,用户翻到第 1000 页(即 LIMIT 10000000, 20)时,接口响应时间可能从几十毫秒飙升至数秒,甚至拖垮数据库 CPU。

这就是经典的**“深分页”(Deep Pagination)**问题。

本文将深入剖析深分页变慢的根本原因,并提供从 SQL 优化、索引技巧到架构演进的五套终极解决方案,助你在大数据量场景下依然保持丝滑的查询体验。


一、病灶诊断:为什么越往后翻越慢?

以 MySQL 为例,执行一条典型的深分页 SQL:

SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 1000000, 20;

1. 执行过程揭秘

MySQL 的执行逻辑并非直接跳过前 100 万条取后 20 条,而是:

  1. 扫描与排序:利用索引(或全表扫描)找到所有满足 user_id = 123 的记录,并按 create_time 排序。
  2. 全盘读取:读取前 1,000,020 条记录。
  3. 丢弃数据:将前 1,000,000 条记录全部丢弃(这些 IO 和 CPU 计算都白费了)。
  4. 返回结果:只返回最后的 20 条记录。

2. 性能瓶颈

  • 回表开销:如果 ORDER BY 的索引没有覆盖所有查询字段(SELECT *),MySQL 需要进行大量的回表操作(从主键索引查完整行数据),这是最耗时的部分。
  • 无效 IO:99% 以上的读取操作都是为了“跳过”,产生了巨大的无效磁盘 IO 和内存消耗。
  • 文件排序:如果索引失效,还需要在 filesort 缓冲区进行大规模排序,极易触发临时表交换到磁盘。

结论:偏移量(Offset)越大,需要扫描和丢弃的数据越多,性能呈线性(甚至指数级)下降。


二、优化方案一:延迟关联(Late Row Lookups)—— 最通用的 SQL 优化

这是在不改变业务逻辑前提下,最有效的 SQL 优化手段。

核心思想

先在索引树上只查出主键 ID(避免回表),过滤掉不需要的数据,然后再通过主键 ID 去原表关联查询完整数据。

优化前

-- 扫描 100 万 + 20 行,回表 100 万 + 20 次
SELECT * FROM orders 
WHERE user_id = 123 
ORDER BY create_time DESC 
LIMIT 1000000, 20;

优化后

-- 1. 子查询:只在覆盖索引上扫描,不回表,丢弃 100 万个 ID 很快
-- 2. 外层查询:只回表 20 次
SELECT t1.* 
FROM orders t1
INNER JOIN (
    SELECT id 
    FROM orders 
    WHERE user_id = 123 
    ORDER BY create_time DESC 
    LIMIT 1000000, 20
) t2 ON t1.id = t2.id;

效果

  • 原理:利用覆盖索引(Covering Index)特性,子查询只需扫描索引树,无需读取行数据,速度极快。外层 Join 仅需回表 20 次。
  • 适用场景:必须使用 OFFSET 且无法改变前端交互的场景。
  • 前提ORDER BY 字段必须有索引,且最好包含在查询条件中。

三、优化方案二:游标法(Seek Method / Keyset Pagination)—— 互联网大厂首选

如果你能控制前端交互(如:禁止用户直接输入页码跳转,只能“下一页”、“上一页”),这是性能最好的方案。

核心思想

不再使用 OFFSET,而是记录上一页最后一条数据的排序字段值(如 ID 或时间),作为下一次查询的起点。

实现方式

假设上一页最后一条数据的 create_time2023-10-01 12:00:00,ID 是 9527

-- 查询下一页 20 条
SELECT * FROM orders 
WHERE user_id = 123 
  AND (create_time < '2023-10-01 12:00:00' 
       OR (create_time = '2023-10-01 12:00:00' AND id < 9527)) -- 防止时间相同
ORDER BY create_time DESC, id DESC
LIMIT 20;

优点

  • 性能恒定:无论翻到第几页,查询效率都一样快(直接定位到索引位置,扫描 20 条即止)。
  • 无深度扫描:完全避免了 OFFSET 带来的扫描浪费。

缺点

  • 无法跳页:用户不能直接跳到第 1000 页,只能一页页翻(这在移动端 App 和无限滚动加载中是完全可接受的)。
  • 数据动态变化:如果在翻页过程中有新数据插入或删除,可能会导致数据重复或遗漏(需根据业务容忍度处理)。

适用场景

  • 移动端 App 信息流、后台管理系统的“下一页”操作、日志查询。
  • Twitter、Facebook、知乎等海量数据产品均采用此方案。

四、优化方案三:限制最大页数 —— 最简单的产品策略

很多时候,深分页是产品需求不合理导致的。

策略

  • 限制页码:规定用户最多只能查看前 100 页(或前 5000 条数据)。
  • 提示引导:当用户尝试访问更深的页面时,提示“为了保障性能,仅展示最近 5000 条数据,请使用搜索功能查找特定内容”。
  • 强制搜索:对于历史久远的数据,强制用户通过时间范围、关键词等精确条件筛选,而不是盲目翻页。

价值

  • 从源头切断深分页请求,保护数据库。
  • 引导用户使用更高效的检索方式。

五、优化方案四:搜索引擎外挂(Elasticsearch)—— 架构级演进

当 MySQL 即使优化后仍无法满足复杂的排序、模糊搜索加分页需求时,应将查询流量迁移到 Elasticsearch (ES)

核心优势

  • 倒排索引:ES 天生为搜索和聚合设计,处理深分页能力远强于 MySQL。
  • Scroll API / Search After
    • ES 提供了 search_after 参数(类似游标法),基于排序字段游标,性能极高且支持深层遍历。
    • Scroll 接口适合全量导出,但不适合实时用户查询。

架构模式

  • 写入:业务写入 MySQL,通过 Canal/Binlog 同步到 ES。
  • 读取:复杂查询、深分页、全文检索走 ES;简单的主键详情查询走 MySQL。

适用场景

  • 电商商品搜索、日志分析平台、内容社区的复杂筛选列表。

六、优化方案五:预计算与冗余表 —— 空间换时间

针对特定的统计类或固定维度的分页,可以提前算好。

策略

  • 大宽表/冗余表:建立一张专门用于列表查询的表,只包含列表展示所需的字段(减少回表),并预先按排序字段排好序。
  • ID 列表缓存:将符合条件的 ID 列表按顺序缓存在 Redis(List 或 ZSet)中。分页时,先从 Redis 取出对应范围的 ID 列表(LRANGE),再回 MySQL 批量查询详情(WHERE id IN (...))。

适用场景

  • 排行榜(Redis ZSet 天然支持)、热门榜单、固定维度的报表。

七、方案对比与选型建议

方案 性能提升 改造成本 用户体验影响 适用场景
延迟关联 ⭐⭐⭐⭐ 低 (改 SQL) 必须支持跳页,且无法引入新组件
游标法 (Seek) ⭐⭐⭐⭐⭐ 中 (改前后端) 不可跳页 移动端信息流、无限滚动、后台列表
限制页数 ⭐⭐⭐ 极低 (改产品) 限制深度 大多数后台管理系统
ES 搜索 ⭐⭐⭐⭐⭐ 高 (引新组件) 无 (甚至更好) 复杂搜索、海量数据、模糊匹配
Redis 缓存 ID ⭐⭐⭐⭐ 中 (改架构) 排行榜、热点数据列表

决策路径

  1. 能否接受“不能跳页”?

    • 能 $\rightarrow$ 游标法(首选,性能最佳)。
    • 不能 $\rightarrow$ 进入下一步。
  2. 是否涉及复杂搜索(模糊、多条件组合)?

    • 是 $\rightarrow$ 引入 Elasticsearch
    • 否 $\rightarrow$ 进入下一步。
  3. 能否限制最大页数(如只查前 5000 条)?

    • 能 $\rightarrow$ 产品限制 + 普通索引优化
    • 不能 $\rightarrow$ 进入下一步。
  4. 最后手段 $\rightarrow$ 延迟关联(SQL 优化)Redis 缓存 ID 列表


八、避坑小贴士

  1. 索引覆盖是关键:确保 ORDER BY 的字段上有索引,且尽量让 WHERE 条件和 ORDER BY 字段共用同一个索引,避免 Filesort。
  2. 避免 SELECT *:只查询列表展示需要的字段,减少网络传输和内存消耗。
  3. Count(*) 优化:深分页常伴随 SELECT COUNT(*)。在大数据量下,精确 Count 也很慢。可以考虑:
    • 使用 ES 的 Count。
    • 使用 Redis 缓存总数。
    • 展示“超过 10000 条”而非精确数字。
    • 利用 MySQL 8.0+ 的统计信息估算。
  4. 监控慢查询:开启 MySQL 慢查询日志(Slow Query Log),定期分析 Limit 偏移量大的 SQL。

结语

大数据量下的分页优化,不仅仅是 SQL 技巧的博弈,更是技术架构产品设计的协同。

  • 对于C 端用户,拥抱“游标法”和“无限滚动”,体验更流畅。
  • 对于B 端管理,合理限制深度,引导精确搜索。
  • 对于海量检索,果断引入 Elasticsearch。

没有银弹,只有最适合当前业务场景的组合拳。通过上述策略,即使是亿级数据表,也能让分页查询重回毫秒级响应。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐