前言
开发中列表查询几乎都要分页,核心两个指标:
pageNum:当前页码(从1开始)pageSize:每页条数
分页两类方案:
- 逻辑分页(内存分页):一次性查出全部数据到内存,在Java内存中截取当前页数据。
- 物理分页(数据库分页) :SQL层面使用
limit(MySQL),只查询当前页需要的数据,推荐生产使用。
重点区分:逻辑分页数据量大时极易OOM;生产环境一律用物理分页。
一、逻辑分页(RowBounds,不推荐生产)
MyBatis自带RowBounds实现逻辑分页。底层是查询全部结果集,在内存中跳过offset、取limit条,不是SQL层面limit。
底层:JDBC游标读取全部数据,在内存做截取,大数据量严重性能问题。
Mapper接口
java
List<User> selectUserList(RowBounds rowBounds);
Mapper.xml
XML
<select id="selectUserList" resultType="User">
select id,name,age from user
</select>
Java调用
java
// offset偏移量,limit每页数量
RowBounds rowBounds = new RowBounds((pageNum-1)*pageSize, pageSize);
List<User> list = userMapper.selectUserList(rowBounds);
优点:不用写limit SQL
缺点:查询全部数据,数据量大内存溢出,生产禁止使用。
二、手写物理分页(原生MyBatis,手写必掌握)
MySQL使用limit offset,size 公式:offset = (pageNum - 1) * pageSize
2.1 Mapper接口
java
// 分页查询用户列表
List<User> selectUserPage(@Param("pageNum") Integer pageNum,
@Param("pageSize") Integer pageSize);
// 查询总条数,用于前端分页组件(总页数、总记录数)
Long selectUserTotal();
2.2 Mapper.xml
XML
<!-- 分页列表 -->
<select id="selectUserPage" resultType="User">
select id,name,age,create_time
from user
order by id desc
limit #{offset}, #{pageSize}
</select>
<!-- 总记录数 -->
<select id="selectUserTotal" resultType="java.lang.Long">
select count(*) from user
</select>
注意:Mapper参数要计算offset
java
int offset = (pageNum - 1) * pageSize;
List<User> records = userMapper.selectUserPage(offset, pageSize);
Long total = userMapper.selectUserTotal();
2.3 封装分页返回实体
java
@Data
public class PageResult<T> {
private List<T> records; // 当前页数据
private Long total; // 总记录数
private Integer pageNum; // 当前页码
private Integer pageSize;// 每页大小
private Integer pages; // 总页数
public static <T> PageResult<T> build(List<T> records, Long total, Integer pageNum, Integer pageSize){
PageResult<T> page = new PageResult<>();
page.setRecords(records);
page.setTotal(total);
page.setPageNum(pageNum);
page.setPageSize(pageSize);
// 计算总页数
long pages = (total + pageSize -1)/pageSize;
page.setPages((int) pages);
return page;
}
}
优点:可控,底层SQL自己掌握,性能好
缺点:每个查询都要手写分页SQL + count语句,重复代码多;不同数据库分页语法不一样(Oracle rownum),移植性差。
三、PageHelper插件(生产最常用)
PageHelper是MyBatis分页插件,基于MyBatis拦截器Interceptor实现 。 原理:在MyBatis执行SQL之前拦截,自动在原始SQL后面拼接limit,同时自动执行count查询。
3.1 引入Maven依赖
XML
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
3.2 SpringBoot yml配置
pagehelper:
helper-dialect: mysql # 指定数据库方言,自动生成对应分页sql
reasonable: true # 合理化分页:pageNum<=1查第一页;pageNum>总页数返回最后一页
support-methods-arguments: false
params: pageNum=pageNum;pageSize=pageSize
3.3 Java代码使用(重点:紧跟查询Mapper方法)
java
// 开启分页,pageNum页码,pageSize每页条数
PageHelper.startPage(pageNum, pageSize);
// 【重要】下一行必须紧跟Mapper查询方法!中间不能有其他查询!
List<User> userList = userMapper.selectAllUser();
// 包装分页结果
PageInfo<User> pageInfo = new PageInfo<>(userList);
PageInfo自带:records、total、pageNum、pageSize、pages、isHasNextPage等分页字段,直接返回前端。
Mapper.xml只需要写普通查询SQL,不需要手写limit
XML
<select id="selectAllUser" resultType="User">
select id,name,age from user order by id desc
</select>
PageHelper核心原理
MyBatis提供拦截器接口Interceptor,拦截Executor的query方法。 调用PageHelper.startPage(),把分页参数存入ThreadLocal。 执行Mapper查询时拦截SQL,取出ThreadLocal分页参数,动态拼接limit语句;同时生成count sql查询总数。查询完成后清理ThreadLocal,防止参数污染。
大坑:
startPage()只对紧跟的第一个Mapper查询生效。中间如果有别的查询,会导致别的SQL被意外分页,造成严重bug!
四、两种分页对比
| 方式 | 类型 | 优点 | 缺点 |
|---|---|---|---|
| RowBounds | 逻辑分页 | 简单不用写limit | 查询全量数据,大数据量OOM,禁止生产 |
| 手写limit | 物理分页 | 可控,性能好 | 每个接口手写分页SQL,重复代码 |
| PageHelper | 物理分页(插件) | 开发快,不用写limit | 必须遵守使用规范,误用容易出现诡异分页bug |
五、生产常见坑点
- PageHelper.startPage()后面不能有其他查询,否则污染其他SQL分页;
- 分页SQL不要写
select *,只查需要字段,提升性能; - 大偏移量问题:
limit 100000,10,MySQL需要扫描前面10万行,性能差。优化:主键id分页where id > 上一页最大id limit 10; - count()优化:尽量命中索引;count(主键)、count(1)、count()在InnoDB下性能差距不大;
- 多表联查分页,注意SQL逻辑,PageHelper会对整个联查SQL加limit;
- 不要在循环里面使用PageHelper。
六、高频问答
Q1:MyBatis RowBounds分页是物理分页吗?
A:不是,属于内存逻辑分页,一次性加载全部数据,内存截取,大数据量会OOM,不推荐生产。
Q2:PageHelper原理是什么?A:基于MyBatis拦截器Interceptor,拦截Executor查询方法;startPage把分页参数放到ThreadLocal;拦截到SQL自动拼接limit,执行count查询;查询后清除ThreadLocal。
Q3:limit大偏移量为什么慢?怎么优化?A:limit offset,size,offset很大时数据库需要扫描并丢弃前面offset行。优化:主键id书签分页(where id > lastId limit pageSize),适合不跳页场景。
Q4:手写分页为什么要查count总数?A:前端分页组件需要总记录数,计算总页数,渲染分页控件。
结语
MyBatis分页优先区分逻辑分页和物理分页。开发项目优先PageHelper提高开发效率,但是必须记住它的使用限制;面试的时候最好能手写原生limit物理分页。大数据量分页要警惕大offset性能问题,采用书签分页优化。分页本质是减少数据库返回的数据量,降低IO压力。