一句话总结
MyBatis 分页两种方式:RowBounds 逻辑分页(查询全部数据,在内存中截取,性能差)和PageHelper 物理分页(拦截 SQL,拼接 LIMIT 子句,只查询当前页数据,推荐)。PageHelper 原理:基于 MyBatis 插件机制,拦截 Executor.query() 方法,在 SQL 执行前动态拼接分页语句。深分页优化:子查询法(先查 ID 再关联)、游标法(记录上次最大 ID)、ES/HBase(换存储引擎)。
初级理解
RowBounds 逻辑分页
# RowBounds:查询全部数据,在内存中截取
# 不推荐!数据量大时 OOM
# Mapper 接口
List<User> selectAll(RowBounds rowBounds);
# 使用
RowBounds rowBounds = new RowBounds(0, 10); // offset=0, limit=10
List<User> users = mapper.selectAll(rowBounds);
# 实际执行的 SQL:
SELECT * FROM user # 查询全部数据!
# 然后在内存中截取前 10 条
# 问题:
# 1. 查询全部数据,网络传输量大
# 2. 内存占用高,数据量大时 OOM
# 3. 性能极差,不推荐使用
PageHelper 物理分页
# PageHelper:拦截 SQL,拼接 LIMIT,只查当前页
# 引入依赖
# <dependency>
# <groupId>com.github.pagehelper</groupId>
# <artifactId>pagehelper-spring-boot-starter</artifactId>
# </dependency>
# 使用(一行代码):
PageHelper.startPage(1, 10); // 第1页,每页10条
List<User> users = mapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
# 实际执行的 SQL:
# 1. SELECT count(0) FROM user -- 查总数
# 2. SELECT * FROM user LIMIT 0, 10 -- 查当前页
# PageInfo 包含:
# pageNum=1(当前页)
# pageSize=10(每页大小)
# total=100(总记录数)
# pages=10(总页数)
# list=[...](当前页数据)
中级深入
PageHelper 原理
# PageHelper 基于 MyBatis 插件机制实现
# 1. PageInterceptor 实现 Interceptor 接口
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}
)
})
public class PageInterceptor implements Interceptor {
public Object intercept(Invocation invocation) {
// 1. 从 ThreadLocal 获取分页参数
Page page = PageHelper.getLocalPage();
if (page == null) {
return invocation.proceed(); // 不分页,直接执行
}
// 2. 先查总数
Long total = queryCount(invocation);
page.setTotal(total);
// 3. 拼接分页 SQL(加 LIMIT)
String pageSql = getPageSql(originalSql, page);
// 4. 执行分页查询
return executePageQuery(pageSql, page);
}
}
# 2. ThreadLocal 存储分页参数
# PageHelper.startPage() → 设置 ThreadLocal
# PageInterceptor.intercept() → 读取 ThreadLocal
# 查询完成后 → 清除 ThreadLocal(防止污染)
# 3. 分页 SQL 拼接(方言适配)
# MySQL: LIMIT offset, limit
# Oracle: ROWNUM 嵌套查询
# PostgreSQL: LIMIT limit OFFSET offset
# SQL Server: OFFSET ... FETCH NEXT ...
PageHelper 注意事项
# 1. startPage 只对紧跟的第一个查询生效
PageHelper.startPage(1, 10);
List<User> users = mapper.selectAll(); // 分页生效
List<Order> orders = mapper.selectAll(); // 分页不生效!
# 2. 安全分页(推荐)
# PageHelper.startPage 可能被其他查询消费
# 使用 lambda 确保安全:
PageInfo<User> pageInfo = PageHelper.startPage(1, 10)
.doSelectPageInfo(() -> mapper.selectAll());
# 3. 分页参数合理化
# pageSizeZero=true:pageSize=0 时查全部
# reasonable=true:pageNum<1 时查第1页,pageNum>pages 时查最后一页
# 4. 不支持嵌套结果映射的分页
# 一对多关联查询时,分页数量不准确
# 解决方案:先分页查主表 ID,再关联查详情
高级进阶
深分页问题与优化
# 深分页问题:
# SELECT * FROM user ORDER BY id LIMIT 1000000, 10
# MySQL 需要扫描前 1000010 行,丢弃前 1000000 行
# 越往后越慢!
# 优化方案1:子查询法(推荐)
SELECT * FROM user
WHERE id >= (SELECT id FROM user ORDER BY id LIMIT 1000000, 1)
ORDER BY id LIMIT 10;
# 先定位起始 ID,再查数据
# 但子查询仍需扫描 1000000 行
# 优化方案2:游标法(最佳)
# 记录上次查询的最大 ID,下次从该 ID 之后查
SELECT * FROM user
WHERE id > #{lastId}
ORDER BY id LIMIT 10;
# 利用主键索引,性能极佳
# 限制:只能顺序翻页,不能跳页
# 优化方案3:覆盖索引 + 延迟关联
SELECT * FROM user u
INNER JOIN (
SELECT id FROM user ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;
# 子查询只查 ID(覆盖索引),再关联查完整数据
# 优化方案4:换存储引擎
# ES:天然支持深分页(search_after)
# HBase:按 rowkey 范围扫描
# TiDB:分布式数据库,深分页性能好
实战场景
# 场景1:Spring Boot + PageHelper 标准用法
@GetMapping("/users")
public PageInfo<UserVO> listUsers(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectByCondition(condition);
// 转换为 VO
List<UserVO> voList = users.stream()
.map(UserVO::from).collect(Collectors.toList());
return new PageInfo<>(voList);
}
# 场景2:游标分页(滚动加载)
@GetMapping("/users/cursor")
public List<User> listByCursor(
@RequestParam(required = false) Long lastId,
@RequestParam(defaultValue = "10") int size) {
return mapper.selectByCursor(lastId, size);
}
# SQL: SELECT * FROM user WHERE id > #{lastId}
# ORDER BY id LIMIT #{size}
# 场景3:MyBatis-Plus 分页
# 配置分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
# 使用
Page<User> page = new Page<>(1, 10);
userMapper.selectPage(page, wrapper);
面试模拟
面试官:MyBatis 分页有哪几种方式?PageHelper 原理是什么?
你:两种:RowBounds 逻辑分页(查全部数据内存截取,不推荐)和 PageHelper 物理分页(拦截 SQL 拼接 LIMIT,推荐)。PageHelper 基于 MyBatis 插件机制,实现 Interceptor 接口拦截 Executor.query(),从 ThreadLocal 获取分页参数,先查 count 再拼接分页 SQL 执行。
面试官:深分页(LIMIT 1000000,10)为什么慢?如何优化?
你:MySQL 需要扫描前 1000010 行再丢弃前 1000000 行,越往后越慢。优化:1)游标法(WHERE id > lastId,利用主键索引,最佳但只能顺序翻页);2)覆盖索引+延迟关联(子查询只查 ID);3)换 ES(search_after)或 HBase。业务上避免深分页,用搜索/筛选替代。