核心概念

  1. 物理分页 vs 逻辑分页

    • 物理分页:数据库层面分页(如MySQL的LIMIT),推荐使用
    • 逻辑分页:应用内存分页(查询所有数据再切片),性能差
  2. MP分页原理

    • 使用PaginationInnerInterceptor插件拦截SQL
    • 自动生成COUNT查询和分页LIMIT语句
    • 返回Page<T>封装对象
  3. 性能瓶颈点

    • 大数据表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;
}

常见错误

  1. 未配置分页插件

    • 现象:分页参数无效,返回所有数据
    • 解决:确保正确配置PaginationInnerInterceptor
  2. 深度分页性能差

    • 现象:LIMIT 1000000, 10 查询缓慢
    • 解决:使用游标分页或ID范围查询
  3. COUNT查询效率低

    • 现象:大数据表COUNT(*)执行慢
    • 解决:使用近似值或关闭COUNT查询
  4. 排序字段未加索引

    • 现象:分页查询全表扫描
    • 解决:为ORDER BY字段添加索引

注意事项

  1. 分页参数验证

    // 防止恶意超大分页
    if (size > 500) {
        size = 500;
    }
    
  2. 索引优化原则

    • WHERE条件和ORDER BY字段必须包含在索引中
    • 联合索引字段顺序需匹配查询条件
  3. **避免SELECT ***:

    • 只查询必要字段减少数据传输
    wrapper.select("id", "name", "create_time");
    
  4. 事务隔离级别

    • 分页查询使用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);
}

性能优化总结

  1. 深度分页

    • 优先使用游标分页(ID连续)
    • 次选延迟关联优化
    • 避免传统OFFSET分页
  2. COUNT优化

    • 不需要总数时关闭COUNT
    • 复杂查询使用自定义COUNT
    • 大数据表使用近似值
  3. 索引优化

    • WHERE和ORDER BY字段必须索引
    • 联合索引字段顺序匹配查询
    • 避免索引失效操作(函数转换等)
  4. 查询优化

    • 只SELECT必要字段
    • 拆分复杂联表查询
    • 大表分页使用分区表
  5. 架构优化

    • 热点数据使用缓存分页
    • 大数据量转用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查询、合理使用索引、减少不必要的数据传输和计算。