在日常开发中,我们经常需要在后台接口中实现分页查询功能,比如展示用户列表、日志记录、订单数据等。如果每次都手动编写分页 SQL,不仅繁琐,还容易出错。PageHelper 作为 MyBatis 常用的分页插件之一,能让分页逻辑变得简单稳定。
本文将带你深入了解 PageHelper 的工作原理、使用方式、常用 API 与最佳实践。
当数据量很小时,直接 SELECT * FROM table 并不会造成明显性能问题。但当数据达到数万、数百万级时,这种查询会迅速拖垮数据库与网络性能。
分页查询的核心思想是:只取出当前页所需的数据,而不是一次性加载全部结果。
在 MyBatis 中,我们可以手写分页 SQL:
SELECT * FROM user LIMIT #{offset}, #{pageSize};
但这种方式存在问题:
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;
这样,分页结果即可包含总记录数与当前页数据,便于前端渲染与分页导航。
假设我们有一张用户表 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;
PageHelper 提供多种分页方法与工具类,下面是常用 API 的实践用法。
最常见的分页方式:
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()); // 当前页数据
适用于偏移量分页场景(例如前端传 offset):
PageHelper.offsetPage(20, 10);
List<User> users = userMapper.selectAll();
对应 SQL:
SELECT * FROM user LIMIT 20,10;
第三个参数控制是否执行 count 查询:
PageHelper.startPage(1, 10, false);
List<User> users = userMapper.selectAll();
此时不会生成 SELECT count(*),在大表场景下能显著提高响应速度。
函数式调用,更加简洁:
Page<User> page = PageHelper.startPage(2, 5)
.doSelectPage(() -> userMapper.selectAll());
System.out.println(page.getResult()); // 当前页数据
System.out.println(page.getTotal()); // 总记录数
直接返回 PageInfo 对象:
PageInfo<User> pageInfo = PageHelper.startPage(1, 10)
.doSelectPageInfo(() -> userMapper.selectAll());
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 设计与索引策略,即可在项目中轻松应对各类分页场景;对于超大数据量,建议优先考虑 “基于主键翻页” 等更高效的替代方案。