数据库性能实战:MySQL 千万级数据“深分页”性能崩塌与 4 种优化解法
【摘要】 在日常业务开发中,LIMIT 1000000, 20 这类深分页语句是导致生产数据库 CPU 飙高和慢查询报警的典型元凶。本文结合 MySQL InnoDB 存储引擎的底层索引结构,深入剖析深分页引发大量“回表”的根本原因,并给出延迟关联、游标寻址、自增主键分流等 4 种工业级优化方案。
一、 为什么深分页会拖垮数据库?
在电商订单、日志流水或消息中心等业务中,常见如下分页查询:
SQL
SELECT * FROM t_order
WHERE user_id = 10086
ORDER BY id DESC
LIMIT 1000000, 20;
很多开发者误以为 MySQL 只扫描了最后这 20 条数据,但实际底层执行逻辑截然不同:
-
扫描总量大:存储引擎需要按索引顺序扫描出前 $1,000,020$ 条记录。
-
海量无用回表:如果查询使用了非聚簇索引(二级索引),且包含了非索引字段(如
SELECT *),引擎每扫描到一条二级索引记录,都需要拿着主键 ID 到主键聚簇索引树上做一次回表查询(Table Lookup)拉取完整行数据。 -
内存抛弃:在 Server 层拿到这 $1,000,020$ 条完整行数据后,将前 $1,000,000$ 条全部丢弃,仅返回最后 20 条。
性能损耗根源:前 100 万次磁盘随机 I/O 回表完全是无用功,直接造成了巨额的 I/O 放大和 CPU 资源空转。
二、 4 种高吞吐优化方案
方案 1:延迟关联(Deferred Join)—— 压榨覆盖索引
如果业务必须支持跳页翻页,推荐使用延迟关联改写 SQL。核心思路是:先利用二级索引在纯内存中完成百万条记录的扫描和裁剪,只对最终需要的 20 条记录执行回表。
SQL
-- 原始慢 SQL(耗时 ~ 4.2s)
SELECT * FROM t_order WHERE user_id = 10086 ORDER BY id DESC LIMIT 1000000, 20;
-- 优化后:延迟关联(耗时 ~ 0.08s)
SELECT o.*
FROM t_order o
INNER JOIN (
-- 子查询仅在覆盖索引树上完成百万级扫描,完全不发生回表
SELECT id
FROM t_order
WHERE user_id = 10086
ORDER BY id DESC
LIMIT 1000000, 20
) AS tmp ON o.id = tmp.id;
-
原理解析:子查询只提取主键
id,命中了联合索引(user_id, id)的覆盖索引(Covering Index)特性,所有排序和过滤全在索引树上完成;外层主查询通过主键等值关联,仅发生 20 次回表,I/O 消耗骤降几个数量级。
方案 2:基于游标的主键寻址(Seek Method / Keyset Pagination)
在移动端无限滚动(Feed 流)、批量数据导出或定时批处理等场景中,用户通常不需要随机跳到“第 50000 页”,此时应彻底废弃
OFFSET 机制,改用游标记录上一页的最后一条主键:SQL
-- 客户端记录上一页最后一条记录的 id 为 9876543
SELECT *
FROM t_order
WHERE user_id = 10086 AND id < 9876543
ORDER BY id DESC
LIMIT 20;
-
原理解析:借助 B+ 树的快速点查能力,直接跳过目标范围外的百万行数据,扫描行数恒定为 20 行,查询性能不受页码深度影响,执行耗时恒定在毫秒级。
方案 3:子查询限制起始 ID
如果业务场景只能顺序分页但不能修改前端 API 入参,可以在 SQL 中将
OFFSET 转化为对连续主键的范围筛选:SQL
SELECT * FROM t_order
WHERE id <= (
SELECT id FROM t_order WHERE user_id = 10086 ORDER BY id DESC LIMIT 1000000, 1
)
ORDER BY id DESC
LIMIT 20;
此方案与延迟关联类似,通过子查询快速定位到第 100 万条记录的主键边界,主查询再利用主键聚簇索引顺序扫描接下来的 20 条记录。
方案 4:架构分流与产品约束
当数据规模进一步膨胀,且存在复杂多字段组合筛选(如按时间段、状态、金额区间等多维度深翻页)时,仅靠 MySQL 单机优化将触碰天花板:
-
产品交互层兜底(UX 约束):限制最大翻页深度(如电商列表页通常只允许展示前 100 页,超出部分提示“请精确搜索条件”)。
-
读写分离与专业引擎承接:通过 Canal 或 CDC 工具将 MySQL 数据实时同步至 Elasticsearch 或 ClickHouse,利用倒排索引或列式存储高效支撑多维筛选与聚合查询。
三、 生产压测基准数据对比
在 1500 万行订单表(InnoDB 引擎,云原生 4C16G 规格)下的压测表现:
| 分页深度 | 原始 SQL 耗时 | 延迟关联优化后 | 游标寻址优化后 | 性能提升倍数 |
| LIMIT 100, 20 | 1.8 ms | 1.9 ms | 0.8 ms | 持平 |
| LIMIT 100,000, 20 | 412 ms | 18 ms | 1.1 ms | 23 ~ 370 倍 |
| LIMIT 1,000,000, 20 | 4,280 ms(触发慢日志) | 84 ms | 1.2 ms | 50 ~ 3500 倍 |
| LIMIT 5,000,000, 20 | 21,500 ms(连接超时) | 390 ms | 1.3 ms | 从超时不可用到毫秒响应 |
四、 生产排查与落地清单
-
检查执行计划:使用
EXPLAIN分析 SQL 时,重点观察Extra字段。若出现Using index说明命中了覆盖索引;若出现Using filesort且伴随大量数据扫描,应优先调整索引覆盖范围或重写为延迟关联。 -
防御大分页攻击:在 API 网关层严格校验前端传入的
pageSize与pageNo,对单次请求数据量设置硬上限(如pageSize <= 100,pageNo * pageSize <= 10000),避免恶意爬虫爬取深层数据打崩数据库连接池。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)