MyBatis 分页原理?

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

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

一句话总结

MyBatis 分页三种方式:RowBounds 逻辑分页(查出全部数据,内存截取,性能差)、PageHelper 物理分页(MyBatis 插件拦截 Executor.query(),自动追加 LIMIT,推荐)、MyBatis-Plus 分页插件(PaginationInnerInterceptor,与 MP 深度集成)。核心原理都是 拦截器 + SQL 改写。深分页优化:游标分页(WHERE id > lastId LIMIT N)替代 OFFSET,避免大 OFFSET 性能问题。

初级理解

三种分页方式对比

方式原理性能推荐度
RowBounds查出全部,内存截取差(全量查询)不推荐
PageHelper拦截器改写 SQL,追加 LIMIT好(物理分页)推荐
MyBatis-Plus内置 PaginationInnerInterceptor好(物理分页)推荐(MP项目)

RowBounds 逻辑分页

# RowBounds 使用方式 RowBounds rowBounds = new RowBounds(0, 10); // offset=0, limit=10 List<User> users = session.selectList( "com.example.mapper.UserMapper.selectAll", null, rowBounds); # RowBounds 原理: # 1. 执行 SELECT * FROM user(查全部数据) # 2. ResultSetHandler 处理结果时 # 3. 根据 RowBounds.offset 和 limit 截取 List # 4. 返回截取后的子列表 # 致命问题: # 1. 查出全部数据到内存,数据量大时 OOM # 2. 网络传输全部数据,浪费带宽 # 3. 数据库压力大(全表扫描) # 结论:生产环境绝对不要用 RowBounds!

中级深入

PageHelper 原理

# PageHelper 核心流程: # 1. PageHelper.startPage(1, 10) # → 创建 Page 对象,设置 pageNum=1, pageSize=10 # → 存入 ThreadLocal(PageHelper.localPage) # 2. 执行 userMapper.selectAll() # → Executor.query() 被 PageInterceptor 拦截 # → 从 ThreadLocal 获取 Page 对象 # → 判断是否需要分页 # 3. 查询总记录数 # → 解析原 SQL:SELECT * FROM user # → 生成 COUNT SQL:SELECT COUNT(*) FROM user # → 执行 COUNT 查询,设置 page.setTotal(count) # 4. 执行分页查询 # → 获取数据库方言(MySQL/Oracle/PostgreSQL) # → MySQL:原 SQL + LIMIT offset, pageSize # → Oracle:嵌套子查询 + ROWNUM # → 执行分页 SQL,返回结果 # 5. 清除 ThreadLocal(finally 块) # → PageHelper.clearPage() # 使用示例: PageHelper.startPage(1, 10); List<User> users = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(users); // pageInfo.getTotal() 总记录数 // pageInfo.getPages() 总页数 // pageInfo.getList() 当前页数据

PageHelper 注意事项

# 1. startPage 只对紧随其后的第一个查询生效 PageHelper.startPage(1, 10); List<User> users = userMapper.selectAll(); // 分页生效 List<Order> orders = orderMapper.selectAll(); // 分页不生效! # 2. 需要分页的查询和 COUNT 查询之间不要有其他查询 # 错误示例: PageHelper.startPage(1, 10); userMapper.selectAll(); // 分页查询 userMapper.count(); // 会清空 ThreadLocal,导致分页失效 # 3. 手动分页(不依赖 startPage) PageHelper.offsetPage(0, 10); // offset=0, limit=10 # 4. 安全分页(限制最大 pageSize) PageHelper.startPage(1, 10, true); // 第三个参数=true 开启安全检查 # 或配置: pagehelper: reasonable: true # 页码<=0 查第一页,页码>总页数查最后一页

高级进阶

深分页性能问题

# 问题:OFFSET 越大,MySQL 需要扫描的行越多 SELECT * FROM user ORDER BY id LIMIT 1000000, 10; # MySQL 需要扫描 1000010 行,丢弃前 1000000 行 # 执行时间随 OFFSET 线性增长 # 解决方案1:游标分页(推荐) # 记录上一页最后一条的 ID,下一页从该 ID 之后开始 SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10; # 利用主键索引,性能恒定 # 实现: public PageResult<User> selectByCursor(Long lastId, int pageSize) { List<User> list = mapper.selectByCursor(lastId, pageSize + 1); boolean hasMore = list.size() > pageSize; if (hasMore) list.remove(list.size() - 1); Long nextLastId = list.isEmpty() ? null : list.get(list.size() - 1).getId(); return new PageResult<>(list, hasMore, nextLastId); } # 游标分页的局限: # 1. 不支持跳页(只能上一页/下一页) # 2. 排序字段必须是唯一且有序的 # 3. 适合 App 端无限滚动场景 # 解决方案2:子查询优化 # 先查 ID,再关联查详情 SELECT * FROM user WHERE id >= ( SELECT id FROM user ORDER BY id LIMIT 1000000, 1 ) ORDER BY id LIMIT 10; # 解决方案3:ES 搜索替代 # 对于复杂搜索+分页,用 Elasticsearch # ES 的 from+size 或 search_after 分页性能远优于 MySQL

MyBatis-Plus 分页插件

# 配置 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); // 单页最大500条 pagination.setOverflow(true); // 溢出处理 interceptor.addInnerInterceptor(pagination); return interceptor; } # 使用 Page<User> page = new Page<>(1, 10); // 方式1:selectPage Page<User> result = userMapper.selectPage(page, wrapper); // 方式2:自定义 SQL + Page 参数 Page<User> result = userMapper.selectByCondition(page, condition); # 自定义 SQL 分页(Mapper.xml) <select id="selectByCondition" resultType="User"> SELECT * FROM user <where> <if test="condition.name != null"> AND name = #{condition.name} </if> </where> ORDER BY id DESC </select> # 注意:不需要手动写 LIMIT,插件自动追加

实战场景

# 场景1:通用分页查询封装 @Data public class PageQuery { private Integer pageNum = 1; private Integer pageSize = 10; private String orderBy; private String orderDirection = "DESC"; } public PageInfo<User> pageQuery(PageQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); if (StringUtils.hasText(query.getOrderBy())) { PageHelper.orderBy(query.getOrderBy() + " " + query.getOrderDirection()); } List<User> users = userMapper.selectByCondition(condition); return new PageInfo<>(users); } # 场景2:大数据量导出(流式查询) # 不用分页,用游标逐批读取 int pageSize = 1000; Long lastId = 0L; while (true) { List<User> batch = mapper.selectByCursor(lastId, pageSize); if (batch.isEmpty()) break; // 处理这批数据 processBatch(batch); lastId = batch.get(batch.size() - 1).getId(); } # 场景3:多表关联分页 # 先分页查主表 ID,再关联查详情 PageHelper.startPage(1, 10); List<Long> ids = userMapper.selectIdsByCondition(condition); // SELECT id FROM user WHERE ... ORDER BY id LIMIT 0,10 List<User> users = userMapper.selectByIds(ids); // SELECT * FROM user WHERE id IN (1,2,3,...)

面试模拟

面试官:MyBatis 有哪些分页方式?PageHelper 原理是什么?

你:三种:RowBounds 逻辑分页(查出全部内存截取,不推荐)、PageHelper 物理分页(推荐)、MyBatis-Plus 分页插件。PageHelper 原理:通过 MyBatis 插件拦截 Executor.query(),从 ThreadLocal 获取分页参数,先执行 COUNT 查询总记录数,再根据数据库方言改写 SQL 追加 LIMIT 子句,最后清除 ThreadLocal。

面试官:深分页(OFFSET 很大)怎么优化?

你:用游标分页替代 OFFSET:WHERE id > lastId ORDER BY id LIMIT N,利用主键索引,性能恒定。缺点是只能上一页/下一页,不支持跳页。也可以用子查询先定位 ID 再关联查详情,或者用 ES 的 search_after 替代深分页。