news 2026/3/8 19:18:35

千万级数据表深分页查询优化:从 5秒 到 0.1秒

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
千万级数据表深分页查询优化:从 5秒 到 0.1秒

摘要:在海量数据的业务场景下,MySQL 的深度分页(Deep Pagination)是一个经典的性能杀手。

1. 事故现场:接口响应超时

上周五临下班,监控系统突然报警,某核心后台管理系统的“订单列表”页面加载超时。运营反馈:“我想翻到第 10000 页查看半年前的历史订单,结果页面一直在转圈,最后报错了。”

我去查了一下数据库日志,发现了一条慢 SQL,执行时间竟然高达5.23 秒

-- 原始慢 SQL SELECT * FROM t_order WHERE status = 1 ORDER BY create_time DESC LIMIT 100000, 10;

看似很简单的查询,为什么会这么慢?

我们先看下表结构和数据量:

  • 表名:t_order

  • 数据量:2000 万行

  • 索引:id (主键), idx_create_time_status (联合索引)

2. 深度分页为什么慢?

很多同学认为 LIMIT 100000, 10 的意思是:直接跳到第 100000 行,然后取出 10 行。

错!MySQL 并不是这么工作的。

LIMIT 100000, 10 的实际执行过程是:

  1. MySQL 会根据索引扫描100010行数据。

  2. 如果不走覆盖索引(Select *),MySQL 还需要拿着这 100010 个主键 ID 去回表(回主键索引查全部字段)。

  3. 抛弃掉前100000行数据。

  4. 只返回最后10行给客户端。

这简直是资源浪费!

大量的回表操作(Random I/O)是导致性能崩塌的罪魁祸首。

3. 解决方案与代码实战

既然知道了瓶颈在于“回表”,那我们的优化思路就是:尽可能减少回表次数,或者干脆不回表

方案一:延迟关联(覆盖索引优化)

这是最通用、改动最小的方案。

核心思路:先通过覆盖索引(Covering Index)只查出目标页的 10 个 ID。因为只查 ID,不需要回表,速度极快。然后再用这 10 个 ID 去关联原表查询完整数据。

优化后的 SQL
SELECT t1.* FROM t_order t1 INNER JOIN ( -- 子查询只查 ID,利用覆盖索引,不用回表 SELECT id FROM t_order WHERE status = 1 ORDER BY create_time DESC LIMIT 100000, 10 ) t2 ON t1.id = t2.id;

执行时间对比:从5.23s降低到了0.15s

在 Java 中,我们不需要手动拼接 SQL,可以利用 MyBatis 的自定义 SQL 功能。

<select id="selectDeepPage" resultType="com.example.entity.Order"> SELECT t1.* FROM t_order t1 INNER JOIN ( SELECT id FROM t_order WHERE status = #{status} ORDER BY create_time DESC LIMIT #{offset}, #{size} ) t2 ON t1.id = t2.id </select>
public PageResult<Order> getOrderPage(int page, int size, int status) { int offset = (page - 1) * size; // 调用优化后的 SQL List<Order> list = orderMapper.selectDeepPage(status, offset, size); // ... 获取总条数 logic return new PageResult<>(list); }

方案二:游标法(Seek Method) —— 性能最佳

如果你的业务场景不需要“跳转到第 N 页”,只需要“下一页”(无限滚动),那么这个方案是性能天花板。

核心思路:记住上一页最后一条数据的排序字段值(比如 create_time 和 id),下一页直接从这个位置开始找。

优化后的 SQL

假设上一页最后一条数据的 create_time 是 '2023-01-01 12:00:00',ID 是 500000。

SELECT * FROM t_order WHERE status = 1 AND (create_time < '2023-01-01 12:00:00' OR (create_time = '2023-01-01 12:00:00' AND id < 500000)) ORDER BY create_time DESC, id DESC LIMIT 10;

执行时间:稳定在0.01s级别,无论翻到多少页。

缺点

  1. 不支持“跳转到第 1000 页”。

  2. 代码逻辑需要改造,前端需传回 last_id。

// cursorTime: 上一页最后一条的创建时间 // cursorId: 上一页最后一条的ID public List<Order> getNextPage(LocalDateTime cursorTime, Long cursorId, int size) { LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Order::getStatus, 1); if (cursorTime != null && cursorId != null) { // 构造 (create_time < t) OR (create_time = t AND id < id) wrapper.and(w -> w .lt(Order::getCreateTime, cursorTime) .or(inner -> inner.eq(Order::getCreateTime, cursorTime).lt(Order::getId, cursorId)) ); } wrapper.orderByDesc(Order::getCreateTime, Order::getId); wrapper.last("LIMIT " + size); return orderMapper.selectList(wrapper); }

方案三:ID 范围查询(限制 ID)

如果是自增 ID 且连续(没有删除过数据)的理想情况,可以直接根据 ID 范围推算。

-- ID 连续,第 10000 页的 ID 起点大约是 10000 * 10 = 100000 SELECT * FROM t_order WHERE id <= 100000 ORDER BY id DESC LIMIT 10;

但这在现实中几乎不可用,因为订单表肯定有删除或 ID 不连续的情况。

一种变体是:先查出第 100000 条数据的 ID(利用覆盖索引快查),再 WHERE id < checked_id。

-- 第一步:极速定位起始 ID SELECT id FROM t_order WHERE status=1 ORDER BY create_time DESC LIMIT 100000, 1; -- 结果得到 id = 9527 -- 第二步:范围查询 SELECT * FROM t_order WHERE status=1 AND id <= 9527 ORDER BY create_time DESC LIMIT 10;

4. 性能压测对比

我们在开发环境模拟了 2000 万数据,对三种方案进行了压测(每组执行 10 次取平均值):

页码深度原始 SQL (秒)延迟关联 (秒)游标法 (秒)
第 10 页0.0050.0060.003
第 1,000 页0.1200.0300.003
第 10,000 页0.8500.0600.004
第 100,000 页5.2300.1500.005

从表格可以看出:

  • 原始 SQL随着页码增加,耗时呈线性甚至指数级增长。

  • 延迟关联(Subquery Join)在深分页时依然保持在毫秒级。

  • 游标法性能最稳,几乎与页码无关。

5. 总结

在 Java 开发中,处理千万级数据的分页查询,千万别无脑用 LIMIT offset, size。

  1. 首选方案延迟关联法(Subquery Join)。它既保留了 PageNumber 跳转的功能,又极大优化了性能。

  2. 极致性能:如果是移动端 Feed 流(瀑布流),请使用游标法(Seek Method)。

  3. 终极武器:如果查询条件非常复杂(涉及多表、模糊搜索),MySQL 可能已经力不从心,建议引入Elasticsearch

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/6 3:08:14

揭秘AI教材生成:低查重秘诀,快速产出专业教材的方法

整理教材知识点真的是一项“精细活”&#xff0c;最让人头痛的就是如何保持平衡与衔接。我们要么担心遗漏了重要的知识点&#xff0c;要么又难以把握合适的难度层次——有的小学教材过于复杂&#xff0c;学生根本无法理解&#xff1b;而高中教材则显得太简单&#xff0c;缺乏必…

作者头像 李华
网站建设 2026/3/3 4:26:22

Sambert与RVC结合:歌声合成新玩法实战演示

Sambert与RVC结合&#xff1a;歌声合成新玩法实战演示 1. 开箱即用的多情感中文语音合成体验 你有没有试过&#xff0c;输入一段歌词&#xff0c;几秒钟后就听到专业级的中文歌声&#xff1f;不是机械朗读&#xff0c;而是带着喜怒哀乐、呼吸停顿、语气起伏的真实人声——甚至…

作者头像 李华
网站建设 2026/3/7 12:29:07

超指数|试填法

lclc3020也可以开平方写&#xff0c;但是效率不如乘法(统计数组元素频次&#xff0c;先处理数字1得到最长奇数长度&#xff0c;再对其余数不断取平方并统计可连续平方的次数计算最长奇数长度的平方链&#xff0c;最终返回最大长度int ans cnt[1] - 1 | 1; // 奇数将数字1的频次…

作者头像 李华
网站建设 2026/3/8 11:42:21

cv_resnet18图片处理慢?推理速度优化实战解决方案

cv_resnet18图片处理慢&#xff1f;推理速度优化实战解决方案 1. 问题定位&#xff1a;为什么cv_resnet18_ocr-detection跑得慢&#xff1f; 你是不是也遇到过这样的情况&#xff1a;上传一张普通尺寸的截图&#xff0c;WebUI界面卡在“检测中…”长达3秒以上&#xff1b;批量…

作者头像 李华
网站建设 2026/2/21 10:03:12

Emotion2Vec+ Large如何重启服务?run.sh脚本执行命令详解

Emotion2Vec Large如何重启服务&#xff1f;run.sh脚本执行命令详解 1. 系统重启前的必要认知 1.1 为什么需要重启服务 Emotion2Vec Large语音情感识别系统在长时间运行后&#xff0c;可能会遇到几种典型情况&#xff1a;模型推理缓存堆积导致响应变慢、WebUI界面卡顿无法刷…

作者头像 李华
网站建设 2026/2/27 11:04:38

Qwen情感分类不精准?System Prompt调优教程

Qwen情感分类不精准&#xff1f;System Prompt调优教程 1. 问题背景&#xff1a;为什么情感分析会“翻车”&#xff1f; 你有没有遇到过这种情况&#xff1a;输入一句明显开心的话&#xff0c;比如“终于搞定项目了&#xff0c;爽&#xff01;”&#xff0c;结果AI却冷冷地告…

作者头像 李华