在日常开发中,我们经常需要在后台接口中实现分页查询功能,比如展示用户列表、日志记录、订单数据等。如果每次都手动编写分页 SQL,不仅繁琐,还容易出错。PageHelper 作为 MyBatis 常用的分页插件之一,能让分页逻辑变得简单稳定。

本文将带你深入了解 PageHelper 的工作原理、使用方式、常用 API 与最佳实践。

为什么需要分页插件

当数据量很小时,直接 SELECT * FROM table 并不会造成明显性能问题。但当数据达到数万、数百万级时,这种查询会迅速拖垮数据库与网络性能。

分页查询的核心思想是:只取出当前页所需的数据,而不是一次性加载全部结果。

在 MyBatis 中,我们可以手写分页 SQL:

SELECT * FROM user LIMIT #{offset}, #{pageSize};

但这种方式存在问题:

PageHelper 正是为了解决这些痛点而生。

PageHelper 简介

PageHelper 是一个开源的 MyBatis 分页插件,由 PageHelper 团队维护。它通过 MyBatis 插件机制自动拦截 SQL,在执行时动态添加分页语句,无需修改原始 SQL。

工作原理解析

PageHelper 的核心在于 MyBatis 拦截器机制。它会在执行查询前拦截并解析 SQL,在末尾追加分页条件,同时在主查询前自动生成一次 count 统计。

执行流程(简化):

示意 SQL:

SELECT count(0) FROM (SQL) tmp_count;
SELECT * FROM (SQL) LIMIT offset, pageSize;

这样,分页结果即可包含总记录数与当前页数据,便于前端渲染与分页导航。

Spring Boot 集成示例

假设我们有一张用户表 user,现在要实现分页查询用户列表。

引入依赖

在 pom.xml 中添加:

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper-spring-boot-starter</artifactId>
  <version>1.4.7</version>
</dependency>

配置文件(可选)

在 application.yml 中添加:

pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

参数说明(常用项):

代码示例

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public PageInfo<User> getUserList(int pageNum, int pageSize) {
        // 1. 开启分页(必须紧邻查询)
        PageHelper.startPage(pageNum, pageSize);
        // 2. 执行查询
        List<User> list = userMapper.selectAll();
        // 3. 封装分页信息
        return new PageInfo<>(list);
    }
}

对应的 Mapper:

@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user ORDER BY id")
    List<User> selectAll();
}

PageHelper 自动生成的 SQL(示例):

SELECT count(0) FROM user;
SELECT * FROM user ORDER BY id LIMIT 0,10;

常用 API 演示

PageHelper 提供多种分页方法与工具类,下面是常用 API 的实践用法。

PageHelper.startPage(pageNum, pageSize)

最常见的分页方式:

PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

System.out.println(pageInfo.getTotal());  // 总记录数
System.out.println(pageInfo.getPages());  // 总页数
System.out.println(pageInfo.getList());   // 当前页数据

PageHelper.offsetPage(offset, limit)

适用于偏移量分页场景(例如前端传 offset):

PageHelper.offsetPage(20, 10);
List<User> users = userMapper.selectAll();

对应 SQL:

SELECT * FROM user LIMIT 20,10;

PageHelper.startPage(pageNum, pageSize, count)

第三个参数控制是否执行 count 查询:

PageHelper.startPage(1, 10, false);
List<User> users = userMapper.selectAll();

此时不会生成 SELECT count(*),在大表场景下能显著提高响应速度。

PageHelper.startPage().doSelectPage()

函数式调用,更加简洁:

Page<User> page = PageHelper.startPage(2, 5)
        .doSelectPage(() -> userMapper.selectAll());

System.out.println(page.getResult());  // 当前页数据
System.out.println(page.getTotal());   // 总记录数

PageHelper.startPage().doSelectPageInfo()

直接返回 PageInfo 对象:

PageInfo<User> pageInfo = PageHelper.startPage(1, 10)
        .doSelectPageInfo(() -> userMapper.selectAll());

PageInfo 常用属性

pageInfo.getList();        // 当前页结果集
pageInfo.getTotal();       // 总记录数
pageInfo.getPageNum();     // 当前页码
pageInfo.getPageSize();    // 每页条数
pageInfo.getPages();       // 总页数
pageInfo.isIsFirstPage();  // 是否第一页
pageInfo.isIsLastPage();   // 是否最后一页

最佳实践与性能优化

分页操作应始终在数据库层完成,避免在 Java 代码中过滤数据,否则会导致分页结果不准确并浪费内存。对于大表或复杂多表 join 的 count 查询,建议手写精简版 SQL,减少不必要的 join,并为过滤条件建立合适的索引,必要时可用缓存或异步统计提升性能。合理化分页(如开启 reasonable=true)可以防止非法页码带来的异常或空页,提升用户体验。

在数据量极大的场景下,推荐使用游标分页或基于主键的 “翻页锚点” 策略(如通过 id > lastId 方式),这样能避免 OFFSET 随页数增大导致的性能下降,并配合稳定的 ORDER BY 字段获得更高查询效率。分页时必须明确指定可索引且稳定的 ORDER BY 字段,否则容易出现翻页抖动或结果不稳定,尤其在并发新增、删除数据时。

常见问题包括分页结果为空,通常是因为 PageHelper.startPage() 没有紧跟查询语句;count 查询慢则需优化 SQL、减少无关 join 或自定义统计;PageHelper 只拦截数据库查询,对 Java Stream 无效,分页应发生在 Mapper 查询阶段。

总结

PageHelper 极大简化了 MyBatis 的分页逻辑,使分页查询更高效、更工程化。掌握其工作机制与常用 API,配合合理的 SQL 设计与索引策略,即可在项目中轻松应对各类分页场景;对于超大数据量,建议优先考虑 “基于主键翻页” 等更高效的替代方案。


FunTester 原创精华


↙↙↙阅读原文可查看相关链接,并与作者交流