当前位置: 首页 > news >正文

大数据量下分页查询性能优化实践(SpringBoot+MyBatis-Plus)

  • 在日常开发中,分页查询是高频需求。但当数据量达到百万、千万级时,普通分页方式往往会出现性能瓶颈。
  • 本文基于SpringBoot+MyBatis-Plus技术栈,从初级到高级逐步讲解大数据量下分页查询的优化方案,帮助大家解决分页性能问题。

一、分页查询的核心挑战

在大数据量场景下,传统分页方式主要面临两个问题:

  1. count查询耗时:为了获取总页数,需要执行select count(*),当表数据量极大时,全表扫描的count操作会非常缓慢。
  2. limit offset性能衰减limit offset, size语法中,offset越大,数据库需要扫描越多的数据(先跳过offset条再取size条),当offset达到十万、百万级时,性能会急剧下降。

二、初级方案:MyBatis-Plus原生分页

MyBatis-Plus提供了便捷的分页插件,适合数据量较小(万级以内)的场景,用法简单直接。

1. 配置分页插件

首先需要在SpringBoot中配置MyBatis-Plus的分页插件:

@Configuration
public class MyBatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件,指定数据库类型(MySQL为例)interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}

2. 基础分页查询

在Service中直接使用Page对象进行分页:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic IPage<User> getUserPage(Integer pageNum, Integer pageSize) {// 创建分页对象(pageNum:页码,pageSize:每页条数)Page<User> page = new Page<>(pageNum, pageSize);// 执行分页查询(条件构造器可根据需求添加)return baseMapper.selectPage(page, null);}
}

3. 优缺点分析

  • 优点:开箱即用,无需手动写limit语句,MyBatis-Plus自动处理分页逻辑。
  • 缺点
    • 自动执行count(*)查询,大数据量下耗时明显。
    • 底层仍依赖limit offset, size,offset增大时性能下降。

三、中级方案:优化count与limit

当数据量达到十万级以上时,初级方案的性能问题开始显现,此时可通过优化count查询和limit用法提升性能。

1. 禁用count查询

如果业务不需要总页数(例如“加载更多”场景),可禁用count查询:

@Override
public IPage<User> getUserPageWithoutCount(Integer pageNum, Integer pageSize) {Page<User> page = new Page<>(pageNum, pageSize);// 禁用count查询page.setOptimizeCountSql(false);page.setSearchCount(false); // 不执行countreturn baseMapper.selectPage(page, null);
}

适用场景:移动端“加载更多”、无需显示总页数的列表页。

2. 直接使用limit语句

MyBatis-Plus的last()方法可直接拼接SQL,避免框架自动生成的复杂分页逻辑:

@Override
public List<User> getUserByLimit(Integer pageNum, Integer pageSize) {Integer offset = (pageNum - 1) * pageSize;// 使用lambdaQuery拼接limitreturn lambdaQuery().last("limit " + offset + "," + pageSize) // 直接拼接limit.list();
}

注意:手动拼接SQL需注意SQL注入风险,offset和pageSize需确保为整数类型。

3. 优化count查询

如果必须要总页数,可优化count查询:

  • 给count查询的条件字段加索引(避免全表扫描)。
  • count(1)替代count(*)(在InnoDB中性能差异不大,但部分场景有优化)。
  • 对于分表场景,可通过汇总各分表count结果减少单表扫描压力。

四、高级方案:游标分页(基于唯一键)

当数据量达到百万级以上,limit offset的性能问题会非常突出(offset=100万时,数据库需要扫描100万+条数据)。此时推荐使用游标分页(Cursor Pagination)。

1. 原理

游标分页基于唯一有序字段(如自增ID、创建时间+唯一键),以上一页的最后一条数据的字段值作为“游标”,定位下一页的起点,避免使用offset。

例如:上一页最后一条数据的ID是1000,下一页就查询id > 1000的前10条数据。

2. 实现(自增ID场景)

@Override
public List<User> getNextPage(Long lastId, Integer pageSize) {// 以lastId为游标,查询下一页return lambdaQuery().gt(lastId != null, User::getId, lastId) // 大于上一页最后一个ID.orderByAsc(User::getId) // 确保排序一致.last("limit " + pageSize).list();
}

调用方式

  • 第一页:lastId = null,查询前N条数据。
  • 后续页:将上一页最后一条数据的ID作为lastId传入。

3. 优缺点分析

  • 优点
    • 性能稳定:无论分页到第几页,都是基于索引的范围查询(id > ?),效率极高。
    • 无重复/漏数据风险:基于唯一键定位,避免因分页过程中数据新增/删除导致的重复或漏数据。
  • 缺点
    • 不支持“跳页查询”(如直接跳转到第100页),仅支持“上一页/下一页”。
    • 依赖有序唯一字段。

五、特殊场景:主键为UUID的处理+游标分页

如果主键是UUID(无序),无法直接作为游标字段,此时需通过以下方式解决:

1. 新增有序唯一字段

在表中新增一个自增序列字段(如sequence_id)或带索引的创建时间字段(create_time),确保其有序且唯一。

// 基于create_time + id(确保唯一)实现游标分页
@Override
public List<User> getNextPageByCreateTime(LocalDateTime lastCreateTime, String lastId, Integer pageSize) {LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();if (lastCreateTime != null && lastId != null) {// 组合条件:create_time > lastCreateTime 或者 (create_time = lastCreateTime 且 id > lastId)queryWrapper.and(w -> w.gt(User::getCreateTime, lastCreateTime).or(w2 -> w2.eq(User::getCreateTime, lastCreateTime).gt(User::getId, lastId)));}return queryWrapper.orderByAsc(User::getCreateTime).orderByAsc(User::getId) // 确保同一时间下的排序唯一.last("limit " + pageSize).list();
}

2. 注意事项

  • create_time需加索引,否则范围查询仍会慢。
  • 必须通过“时间+ID”组合确保唯一性,避免同一时间有多个数据时的分页错乱。

六、极致优化:结合业务场景的方案

  1. 分库分表场景

    • 用Sharding-JDBC等中间件,分页时先从各分表获取分页数据,再聚合结果。
    • 避免跨库count(可通过估算或缓存总条数)。
  2. 缓存热门分页

    • 对首页、前几页等高频访问的分页结果进行缓存(如Redis)。
    • 缓存时间根据数据更新频率调整。
  3. 异步预加载

    • 当用户浏览第N页时,异步预加载第N+1页数据,提升用户体验。

七、方案选择建议

数据量推荐方案适用场景
万级以内MyBatis-Plus原生分页后台管理系统、需总页数展示
十万级禁用count+优化limit无需总页数的列表页
百万级以上游标分页移动端加载更多、大数据列表

总结

大数据量下的分页优化核心是减少扫描数据量避免全表操作。实际开发中,需根据数据量、业务场景(是否需要跳页、总页数)选择合适的方案,而非盲目追求“高级方案”。结合索引优化、缓存策略等手段,才能真正实现高性能的分页查询。

http://www.lryc.cn/news/617236.html

相关文章:

  • 集成电路学习:什么是URDF Model统一机器人描述格式模型
  • ZeroNews:如何构建安全(无需 V*N!)的工业物联网连接
  • 大模型落地:AI 技术重构工作与行业的底层逻辑
  • Salesforce案例:零售企业会员积分体系
  • 【软考架构】需求工程中,系统分析与设计的结构化方法
  • [Shell编程] Shell 编程之免交互
  • C语言模拟 MCU 上电后程序的执行顺序 + 回调函数机制 + 程序计数器(PC)和堆栈的作用
  • LangVM —— 一站式多语言版本管理工具,让 Java、Python、Go、Node.js 切换更丝滑
  • CVE-2019-0708复刻
  • buildroot编译qt 5.9.8 arm64版本踩坑
  • Windows文件时间修改指南:从手动到自动化
  • AI驱动的智能编码革命:从Copilot到全流程开发自动化
  • FFmepg源码系列-avformat_open_input()
  • Python调用C/C++函数库的多种方法与实践指南
  • 聊天室全栈开发-保姆级教程(Node.js+Websocket+Redis+HTML+CSS)
  • MathType关联Wps实现公式编辑【Tex语法适配】
  • 2438. 二的幂数组中查询范围内的乘积
  • 【liunx】web高可用---nginx
  • 编译Android版本可用的高版本iproute2
  • 机器学习 - Kaggle项目实践(1)Titanic
  • C++多态详解
  • SDI设计中,为何SD-SDI模式下,接收器用DRU实现,在3G-SDI模式下,使用transceiver实现
  • 多轮会话记忆的核心挑战
  • Spring Boot 中 @Transactional 解析
  • 自动化备份全网服务器数据平台项目
  • P2865 [USACO06NOV] Roadblocks G
  • ListNode* dummy = new ListNode();什么意思
  • 【功能测试】软件集成测试思路策略与经验总结
  • 使用纯NumPy实现回归任务:深入理解机器学习本质
  • 小结: getSpringFactoriesInstances从 `spring.factories` 文件中加载和实例化指定类型的类