# MySQL 中如何解决深度分页的问题?

9 min read
Table of Contents

MySQL 中如何解决深度分页的问题?

MySQL 深度分页问题解决方案

【核心定义】

深度分页问题是指在使用 LIMIT offset, size 语法进行大数据量分页查询时,随着 offset 值的增大,查询性能急剧下降的现象。

【关键要点】

  1. 问题本质:MySQL 需要先扫描并跳过 offset 条记录,才能返回后续的 size 条记录,导致大量无效的 I/O 和 CPU 消耗。
  2. 性能拐点:当 offset > 10000 时性能下降明显,offset > 100000 时查询可能达到秒级甚至分钟级。
  3. 核心矛盾:传统分页的 OFFSET 机制与大数据量查询的效率冲突。

【深度推导/细节】

传统分页的性能瓶颈分析

-- 问题查询:查询第 10000 页,每页 20 条
SELECT * FROM table ORDER BY id LIMIT 200000, 20;

执行过程拆解:

  • Step 1: MySQL 需要先读取 200000 + 20 条完整记录到内存
  • Step 2: 对这 200020 条记录进行排序(如果有 ORDER BY)
  • Step 3: 丢弃前 200000 条,只返回最后 20 条
  • Step 4: 实际有效数据仅 20 条,但处理了 200020 条

解决方案逻辑推导

方案一:基于主键/索引的优化(最优解)

-- 优化后查询
SELECT * FROM table
WHERE id > 上一页最后一条记录的id
ORDER BY id
LIMIT 20;

原理拆解:

  • 利用索引的有序性,直接定位到起始位置
  • 避免了 OFFSET 的扫描跳过过程
  • 时间复杂度从 O(N) 降为 O(logN)

方案二:子查询优化

SELECT * FROM table
WHERE id >= (
SELECT id FROM table
ORDER BY id
LIMIT 200000, 1
)
LIMIT 20;

执行逻辑:

  1. 子查询先通过索引快速定位到第 200000 条记录的主键
  2. 外层查询基于该主键进行范围查询
  3. 避免了全表扫描,利用了索引覆盖

方案三:业务层优化 - 禁止深度跳转

  • 产品层面:限制只能查看前 100 页,或提供精确搜索
  • 技术层面:使用搜索引擎(Elasticsearch)处理深度分页需求

方案四:游标分页(Cursor-based Pagination)

-- 第一页
SELECT * FROM table ORDER BY created_at DESC, id DESC LIMIT 20;
-- 下一页(客户端传递最后一条记录的 created_at 和 id)
SELECT * FROM table
WHERE (created_at < '最后时间' OR (created_at = '最后时间' AND id < '最后ID'))
ORDER BY created_at DESC, id DESC
LIMIT 20;

【关联/对比】

MySQL 不同版本优化差异

版本优化特性对分页的影响
MySQL 5.6索引条件下推对子查询优化有帮助
MySQL 5.7生成列索引可对复杂排序条件建立索引
MySQL 8.0窗口函数可使用 ROW_NUMBER() 进行高效分页
MySQL 8.0倒序索引扫描优化对 DESC 排序的分页性能提升

与其他技术方案对比

方案适用场景限制条件
MySQL 主键优化主键有序且连续需要业务接受不能随机跳页
Elasticsearch海量数据搜索分页数据同步延迟,维护成本高
Redis ZSET排行榜类场景数据量受内存限制
业务层缓存热点数据分页数据实时性要求不高

MySQL 8.0 窗口函数方案

-- 使用 ROW_NUMBER() 进行高效分页
WITH numbered_rows AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY id) as row_num
FROM table
)
SELECT * FROM numbered_rows
WHERE row_num BETWEEN 200001 AND 200020;

优势:

  • 逻辑清晰,符合 SQL 标准
  • 在某些场景下比传统 LIMIT 更高效
  • 支持复杂的排序规则

【面试官追问】

追问1:为什么 LIMIT 1000000, 10 比 LIMIT 0, 10 慢这么多?

技术事实:

  • LIMIT 0, 10:只需读取前 10 条符合条件的记录
  • LIMIT 1000000, 10:需要先读取 1000010 条记录,然后丢弃前 1000000 条
  • 性能差异主要来自:
    1. I/O 成本:需要读取更多数据页到内存
    2. CPU 成本:需要对更多记录进行排序和比较
    3. 内存压力:可能触发临时表写入磁盘

追问2:如何设计一个支持随机跳转的高效分页系统?

分层解决方案:

  1. 前 N 页(如 1-100):使用传统 LIMIT,性能可接受
  2. 中间页(如 101-1000):使用主键/索引优化方案
  3. 深度页(>1000):强制使用搜索条件或引导用户使用其他方式
  4. 技术组合:
    • 热点数据:Redis 缓存分页结果
    • 复杂查询:Elasticsearch 二级索引
    • 实时性要求高:使用数据库从库专门处理分页查询

追问3:ORDER BY 对分页性能的影响?

关键事实:

  • 无索引的 ORDER BY 会导致 filesort,大数据量时性能极差
  • 解决方案:
    1. 为排序字段建立索引
    2. 使用覆盖索引避免回表
    3. 对于复合排序,建立联合索引
    4. MySQL 8.0+ 可利用倒序索引优化 DESC 排序

追问4:分页查询的并发性能如何优化?

架构级方案:

  1. 读写分离:分页查询走从库,避免影响主库写性能
  2. 连接池优化:设置合理的超时时间,避免长查询占用连接
  3. 查询拦截:监控并拦截执行时间过长的分页查询
  4. 结果集缓存:对固定条件的分页结果进行缓存

【性能数字与临界点】

关键性能阈值

  1. offset < 1000:性能良好,可直接使用传统 LIMIT
  2. 1000 < offset < 10000:建议使用子查询优化
  3. offset > 10000:必须使用主键/索引优化或业务层方案
  4. offset > 100000:传统分页已不可用,需架构级解决方案

优化效果对比

数据量传统 LIMIT主键优化性能提升
10万条0.5s0.01s50倍
100万条5s0.02s250倍
1000万条50s+0.03s1000倍+

【最佳实践总结】

选择策略决策树

是否需要随机跳转?
├── 否 → 使用主键/游标分页(最优性能)
└── 是 →
├── 数据量 < 100万 → 子查询优化
├── 100万 < 数据量 < 1000万 → 业务层限制 + 搜索优化
└── 数据量 > 1000万 → Elasticsearch + 缓存层

代码示例模板

-- 方案1:主键连续场景
SELECT * FROM users
WHERE id > ?last_id
ORDER BY id
LIMIT ?page_size;
-- 方案2:需要随机跳转
SELECT * FROM users
WHERE id >= (
SELECT id FROM users
ORDER BY id
LIMIT ?offset, 1
)
LIMIT ?page_size;
-- 方案3:MySQL 8.0+ 窗口函数
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY id) as rn
FROM users
) t WHERE rn BETWEEN ?start AND ?end;

监控与告警指标

  1. 查询时间:> 1s 的分页查询需要告警
  2. 扫描行数:扫描行数/返回行数 > 100 需要优化
  3. 内存使用:分页查询导致临时表写入磁盘需要优化
  4. 锁等待:分页查询造成锁竞争需要调整索引或架构
My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# MySQL 中的事务隔离级别有哪些?

7 min read

事务隔离级别是数据库管理系统用于控制多个并发事务之间可见性和相互影响程度的一组标准规则,其本质是在数据一致性、并发性能和数据准确性三者之间进行权衡。

Read

Comments