MyBatis 分页原理?

2025年 阅读约 10 分钟 面试指南 · MyBatis

深入解析MyBatis分页原理:RowBounds逻辑分页(内存分页)、PageHelper物理分页(SQL分页)、深分页优化方案、分页插件原理,附面试模拟问答。

一句话总结

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。业务上避免深分页,用搜索/筛选替代。