在 Web 开发中,分页是处理大量数据展示的必备功能。MyBatis 本身不强制提供分页实现,但支持多种分页策略,主要分为 逻辑分页(内存分页) 和 物理分页(数据库分页)。理解它们的原理与适用场景,对系统性能和稳定性至关重要。
一、逻辑分页:MyBatis 自带的 RowBounds
RowBounds 是 MyBatis 提供的内置分页工具,属于逻辑分页——即先查询出全部结果集,再在 Java 内存中截取指定范围的数据。
使用示例:
// Mapper 接口(无需特殊定义) List<User> selectAllUsers(); // Service 层调用 int offset = (pageNum - 1) * pageSize; // 起始行 int limit = pageSize; // 获取条数 RowBounds rowBounds = new RowBounds(offset, limit); List<User> users = userMapper.selectAllUsers(rowBounds);
底层原理:
- 执行原始 SQL(如
SELECT * FROM user),获取完整ResultSet; - MyBatis 遍历结果集,跳过
offset条,再取limit条; - 未生成带
LIMIT的 SQL,数据库仍返回全部数据。
⚠️ 缺点:
- 内存开销大:若表有 100 万条数据,即使只取第 1 页,也会加载全部到内存;
- 性能差:大数据量下响应慢,甚至引发
OutOfMemoryError;- 网络传输浪费:大量无用数据在网络中传输。
✅ 适用场景:
仅适用于数据量极小(如配置表、字典表)且分页需求简单的场景。
二、物理分页:真正的高效分页
物理分页通过在 SQL 层面添加分页语句(如 LIMIT、ROWNUM),让数据库只返回所需数据,大幅减少 I/O、内存和网络开销。
常见的物理分页实现方式包括:
1. 手写 SQL 分页
直接在 Mapper XML 中编写带分页关键字的 SQL(需适配不同数据库):
<!-- MySQL --> <select id="selectUsersByPage" resultType="User"> SELECT * FROM user LIMIT #{offset}, #{limit} </select>
<!-- Oracle --> <select id="selectUsersByPage" resultType="User"> SELECT * FROM ( SELECT ROWNUM rn, t.* FROM ( SELECT * FROM user ORDER BY id ) t WHERE ROWNUM <= #{endRow} ) WHERE rn > #{startRow} </select>
✅ 优点:完全可控;
❌ 缺点:需为不同数据库维护不同 SQL,维护成本高。
2. 第三方插件:PageHelper(推荐)
PageHelper 是最流行的 MyBatis 分页插件,自动识别数据库类型并重写 SQL。
使用步骤:
- 引入依赖(Maven):
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency>
- 在查询前开启分页:
PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.selectAllUsers(); // 原始查询方法,无需改写
- 获取分页信息(可选):
PageInfo<User> pageInfo = new PageInfo<>(users); long total = pageInfo.getTotal(); // 总记录数
插件原理:
- 拦截 SQL 执行;
- 自动拼接分页语句(MySQL →
LIMIT,Oracle →ROWNUM,SQL Server →OFFSET FETCH等); - 额外执行一条
COUNT(*)查询总条数。
✅ 优势:
- 一行代码实现分页;
- 自动适配主流数据库;
- 返回完整分页元数据(总页数、是否首页等)。
3. 数组分页(伪物理分页)
在 DAO 层查出全部数据,Service 层用 List.subList() 截取:
public List<Student> queryStudentsByArray(int currPage, int pageSize) { List<Student> all = studentMapper.queryAll(); int start = (currPage - 1) * pageSize; int end = Math.min(start + pageSize, all.size()); return all.subList(start, end); }
⚠️ 本质仍是逻辑分页!只是手动实现了
RowBounds的功能,同样存在内存溢出风险,不推荐用于生产环境。
4. 自定义拦截器分页
通过实现 MyBatis Interceptor 接口,拦截特定命名的查询(如 *ByPage),动态拼接 LIMIT。
✅ 适合有统一规范的团队;
❌ 开发成本较高,需处理 SQL 解析、方言适配等细节。
三、逻辑分页 vs 物理分页:对比总结
| 对比项 | 逻辑分页(RowBounds) | 物理分页(PageHelper / 手写 SQL) |
| 数据加载 | 全量加载到内存 | 数据库只返回当前页 |
| 内存占用 | 高(随总数据量增长) | 低(仅当前页) |
| 网络传输 | 大量无用数据 | 仅有效数据 |
| 性能(大数据) | 极差,可能 OOM | 优秀 |
| 数据库压力 | 高(全表扫描) | 低(利用索引+分页) |
| 适用场景 | 小表、配置类数据 | 所有业务主表 |
📌 核心原则:
物理分页总是优先于逻辑分页。除非数据量极小(< 1000 条),否则应避免使用
RowBounds或subList分页。
通过合理选择分页策略,尤其是采用 PageHelper 等物理分页插件,你可以在保证代码简洁的同时,获得高性能、高稳定性的分页能力,为应用打下坚实的数据访问基础。