大数据量下分页查询性能优化实践(SpringBoot+MyBatis-Plus)
- 在日常开发中,分页查询是高频需求。但当数据量达到百万、千万级时,普通分页方式往往会出现性能瓶颈。
- 本文基于SpringBoot+MyBatis-Plus技术栈,从初级到高级逐步讲解大数据量下分页查询的优化方案,帮助大家解决分页性能问题。
一、分页查询的核心挑战
在大数据量场景下,传统分页方式主要面临两个问题:
- count查询耗时:为了获取总页数,需要执行
select count(*)
,当表数据量极大时,全表扫描的count操作会非常缓慢。 - 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”组合确保唯一性,避免同一时间有多个数据时的分页错乱。
六、极致优化:结合业务场景的方案
-
分库分表场景:
- 用Sharding-JDBC等中间件,分页时先从各分表获取分页数据,再聚合结果。
- 避免跨库count(可通过估算或缓存总条数)。
-
缓存热门分页:
- 对首页、前几页等高频访问的分页结果进行缓存(如Redis)。
- 缓存时间根据数据更新频率调整。
-
异步预加载:
- 当用户浏览第N页时,异步预加载第N+1页数据,提升用户体验。
七、方案选择建议
数据量 | 推荐方案 | 适用场景 |
---|---|---|
万级以内 | MyBatis-Plus原生分页 | 后台管理系统、需总页数展示 |
十万级 | 禁用count+优化limit | 无需总页数的列表页 |
百万级以上 | 游标分页 | 移动端加载更多、大数据列表 |
总结
大数据量下的分页优化核心是减少扫描数据量和避免全表操作。实际开发中,需根据数据量、业务场景(是否需要跳页、总页数)选择合适的方案,而非盲目追求“高级方案”。结合索引优化、缓存策略等手段,才能真正实现高性能的分页查询。