数据库性能实战:MySQL 千万级数据“深分页”性能崩塌与 4 种优化解法

举报
yd_243452805 发表于 2026/10/27 08:26:19 2026/10/27
【摘要】 在日常业务开发中,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. 扫描总量大:存储引擎需要按索引顺序扫描出前 $1,000,020$ 条记录。

  2. 海量无用回表:如果查询使用了非聚簇索引(二级索引),且包含了非索引字段(如 SELECT *),引擎每扫描到一条二级索引记录,都需要拿着主键 ID 到主键聚簇索引树上做一次回表查询(Table Lookup)拉取完整行数据。

  3. 内存抛弃:在 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 单机优化将触碰天花板:

  1. 产品交互层兜底(UX 约束):限制最大翻页深度(如电商列表页通常只允许展示前 100 页,超出部分提示“请精确搜索条件”)。

  2. 读写分离与专业引擎承接:通过 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 从超时不可用到毫秒响应

四、 生产排查与落地清单

  1. 检查执行计划:使用 EXPLAIN 分析 SQL 时,重点观察 Extra 字段。若出现 Using index 说明命中了覆盖索引;若出现 Using filesort 且伴随大量数据扫描,应优先调整索引覆盖范围或重写为延迟关联。

  2. 防御大分页攻击:在 API 网关层严格校验前端传入的 pageSize 与 pageNo,对单次请求数据量设置硬上限(如 pageSize <= 100,pageNo * pageSize <= 10000),避免恶意爬虫爬取深层数据打崩数据库连接池。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。