核心概念
物理分页 vs 逻辑分页:
- 物理分页:数据库层面分页(如MySQL的LIMIT),推荐使用
- 逻辑分页:应用内存分页(查询所有数据再切片),性能差
MP分页原理:
- 使用
PaginationInnerInterceptor
插件拦截SQL - 自动生成COUNT查询和分页LIMIT语句
- 返回
Page<T>
封装对象
- 使用
性能瓶颈点:
- 大数据表COUNT(*)效率低
- 深度分页(offset过大)性能差
- 复杂联表查询分页效率低
详细操作步骤
步骤1:配置分页插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件配置(推荐MySQL)
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L); // 单页最大记录数
paginationInterceptor.setOverflow(true); // 超出总页数后返回首页
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
步骤2:基础分页查询
// Service层
public Page<User> getUsersByPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1).orderByDesc("create_time");
return userMapper.selectPage(page, wrapper);
}
// Controller层
@GetMapping("/users")
public PageResult<User> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
Page<User> pageResult = userService.getUsersByPage(page, size);
return new PageResult<>(
pageResult.getRecords(),
pageResult.getTotal(),
pageResult.getCurrent(),
pageResult.getSize()
);
}
步骤3:优化COUNT查询
// 方法1:关闭COUNT查询(当不需要总数时)
Page<User> page = new Page<>(current, size);
page.setSearchCount(false); // 不执行COUNT查询
// 方法2:自定义COUNT语句(复杂查询时)
@Select("SELECT COUNT(*) FROM user WHERE dept_id = #{deptId}")
Long customCount(@Param("deptId") Long deptId);
public Page<User> getDeptUsers(Long deptId, int page, int size) {
Page<User> pageInfo = new Page<>(page, size);
pageInfo.setSearchCount(false); // 禁用自动COUNT
// 执行分页查询
List<User> records = userMapper.selectDeptUsers(pageInfo, deptId);
pageInfo.setRecords(records);
// 使用自定义COUNT查询
Long total = userMapper.customCount(deptId);
pageInfo.setTotal(total);
return pageInfo;
}
步骤4:深度分页优化(游标分页)
// 基于ID的游标分页(避免OFFSET)
public PageResult<User> getUsersByCursor(Long lastId, int size) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt(lastId != null, "id", lastId)
.orderByAsc("id")
.last("LIMIT " + size);
List<User> records = userMapper.selectList(wrapper);
// 获取最后一个ID用于下次分页
Long nextLastId = null;
if (!records.isEmpty()) {
nextLastId = records.get(records.size() - 1).getId();
}
return new PageResult<>(records, nextLastId);
}
// 使用示例
@GetMapping("/users/cursor")
public PageResult<User> getUsersCursor(
@RequestParam(required = false) Long lastId,
@RequestParam(defaultValue = "10") int size) {
return userService.getUsersByCursor(lastId, size);
}
步骤5:联表分页优化
// 先分页查主表ID,再关联查询
public Page<OrderVO> getOrderPage(int page, int size) {
// 1. 分页查询订单ID
Page<Order> idPage = new Page<>(page, size);
idPage.setSearchCount(true); // 需要总数
userMapper.selectPage(idPage,
new QueryWrapper<Order>().select("id").orderByDesc("create_time"));
// 2. 提取ID列表
List<Long> orderIds = idPage.getRecords().stream()
.map(Order::getId)
.collect(Collectors.toList());
if (orderIds.isEmpty()) {
return new Page<>(idPage.getCurrent(), idPage.getSize(), idPage.getTotal());
}
// 3. 关联查询订单详情
List<OrderVO> orderVOS = orderMapper.selectOrderWithDetails(orderIds);
// 4. 组装分页结果
Page<OrderVO> resultPage = new Page<>(page, size, idPage.getTotal());
resultPage.setRecords(orderVOS);
return resultPage;
}
常见错误
未配置分页插件:
- 现象:分页参数无效,返回所有数据
- 解决:确保正确配置
PaginationInnerInterceptor
深度分页性能差:
- 现象:
LIMIT 1000000, 10
查询缓慢 - 解决:使用游标分页或ID范围查询
- 现象:
COUNT查询效率低:
- 现象:大数据表COUNT(*)执行慢
- 解决:使用近似值或关闭COUNT查询
排序字段未加索引:
- 现象:分页查询全表扫描
- 解决:为ORDER BY字段添加索引
注意事项
分页参数验证:
// 防止恶意超大分页 if (size > 500) { size = 500; }
索引优化原则:
- WHERE条件和ORDER BY字段必须包含在索引中
- 联合索引字段顺序需匹配查询条件
**避免SELECT ***:
- 只查询必要字段减少数据传输
wrapper.select("id", "name", "create_time");
事务隔离级别:
- 分页查询使用
READ COMMITTED
避免锁竞争
- 分页查询使用
使用技巧
1. 智能分页模式切换
public Page<User> smartPageQuery(int page, int size) {
Page<User> pageParam = new Page<>(page, size);
// 深度分页使用游标模式
if (page > 1000) {
return cursorPageQuery(pageParam);
}
return userMapper.selectPage(pageParam, null);
}
2. 分页缓存策略
@Cacheable(value = "userPage", key = "#page + '_' + #size + '_' + #queryHash")
public Page<User> getCachedPage(int page, int size, String queryHash) {
// ... 分页查询逻辑
}
3. 并行查询优化
CompletableFuture<Long> countFuture = CompletableFuture.supplyAsync(
() -> userMapper.selectCount(wrapper), executor);
CompletableFuture<List<User>> dataFuture = CompletableFuture.supplyAsync(
() -> userMapper.selectList(wrapper.last("LIMIT " + offset + "," + size)), executor);
// 等待结果
Page<User> page = new Page<>(pageNum, size);
page.setRecords(dataFuture.join());
page.setTotal(countFuture.join());
最佳实践与性能优化
1. 深度分页优化方案
方案1:ID范围分页
SELECT * FROM orders
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{size}
方案2:延迟关联
SELECT * FROM orders
INNER JOIN (
SELECT id FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 1000000, 10
) AS tmp USING(id)
2. COUNT查询优化策略
方案 | 适用场景 | 实现方式 |
---|---|---|
精确COUNT | 小数据表 | SELECT COUNT(*) |
近似COUNT | 允许误差的大数据表 | SHOW TABLE STATUS |
计数表 | 频繁分页的静态表 | 维护专用计数表 |
分页缓存 | 变化不频繁的数据 | 缓存前N页的总数 |
3. 分布式ID分页优化
// 使用Snowflake等分布式ID
wrapper.gt(lastId != null, "distributed_id", lastId)
.orderByAsc("distributed_id");
4. 分页监控与调优
// 自定义分页拦截器监控性能
public class PerformancePaginationInterceptor extends PaginationInnerInterceptor {
@Override
protected void afterQuery(Page<?> page, Object parameterObject, Result<?> result) {
long queryTime = System.currentTimeMillis() - startTime;
if (queryTime > 1000) {
logger.warn("Slow page query: {}ms, SQL: {}", queryTime, getOriginalSql());
}
}
}
5. Elasticsearch集成
// 大数据量分页转用ES
public Page<User> searchUsers(String keyword, int page, int size) {
return elasticsearchTemplate.queryForPage(
new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withPageable(PageRequest.of(page, size))
.build(),
User.class);
}
性能优化总结
深度分页:
- 优先使用游标分页(ID连续)
- 次选延迟关联优化
- 避免传统OFFSET分页
COUNT优化:
- 不需要总数时关闭COUNT
- 复杂查询使用自定义COUNT
- 大数据表使用近似值
索引优化:
- WHERE和ORDER BY字段必须索引
- 联合索引字段顺序匹配查询
- 避免索引失效操作(函数转换等)
查询优化:
- 只SELECT必要字段
- 拆分复杂联表查询
- 大表分页使用分区表
架构优化:
- 热点数据使用缓存分页
- 大数据量转用Elasticsearch
- 历史数据归档减少主表体积
完整优化示例
public PageResult<OrderVO> optimizedOrderPage(OrderQuery query, int page, int size) {
// 1. 参数校验
if (size > 200) size = 200;
// 2. 深度分页使用游标模式
if (page > 1000 && query.getLastId() != null) {
return cursorOrderPage(query, query.getLastId(), size);
}
// 3. 创建分页对象(禁用自动COUNT)
Page<Order> pageParam = new Page<>(page, size);
pageParam.setSearchCount(false);
// 4. 构建查询条件
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.select("id", "order_no", "create_time")
.eq(query.getStatus() != null, "status", query.getStatus())
.gt(query.getStartTime() != null, "create_time", query.getStartTime())
.orderByDesc("create_time");
// 5. 分页查询主键
List<Order> orders = orderMapper.selectList(pageParam, wrapper);
// 6. 提取ID并关联查询
List<Long> orderIds = orders.stream().map(Order::getId).collect(Collectors.toList());
List<OrderVO> vos = orderIds.isEmpty()
? Collections.emptyList()
: orderMapper.selectOrderVOs(orderIds);
// 7. 异步获取总数(首次查询或需要时)
CompletableFuture<Long> countFuture = CompletableFuture.supplyAsync(() -> {
if (page == 1 || query.isNeedTotal()) {
return orderMapper.selectCount(wrapper);
}
return null;
}, taskExecutor);
// 8. 构建结果
PageResult<OrderVO> result = new PageResult<>();
result.setData(vos);
result.setPage(page);
result.setSize(size);
// 非阻塞设置总数
countFuture.thenAccept(total -> {
if (total != null) {
result.setTotal(total);
result.setTotalPages((total + size - 1) / size);
}
});
return result;
}
通过以上优化策略,MyBatis-Plus分页查询性能可提升数倍至数十倍,特别是在大数据量和高并发场景下效果显著。核心要点包括:避免深度OFFSET分页、优化COUNT查询、合理使用索引、减少不必要的数据传输和计算。