MyBatis-Plus分页查询实战:从配置到性能优化的完整指南
目录导读
- 为什么分页查询是Web开发的“刚需”
- MyBatis-Plus分页插件核心配置(附代码)
- 三种分页写法对比:传统 vs 内置 vs 自定义
- 高频面试题:分页SQL的常见坑(问与答)
- 性能调优:百万级数据分页不卡顿的5个技巧
- 什么时候该用物理分页,什么时候该用逻辑分页
为什么分页查询是Web开发的“刚需”
想象一下,你的用户表有500万条记录,如果一次性全查出来,浏览器直接崩溃——这不是段子,是线上事故,分页查询的核心价值在于减少数据传输量和降低数据库负荷。

MyBatis-Plus作为国内最流行的MyBatis增强框架,把分页做成了“开箱即用”的功能,但很多新手只知其然不知其所以然,今天我们就通过一个完整的电商订单分页案例,把分页的底层逻辑、性能调优、常见坑一次性讲透。
MyBatis-Plus分页插件核心配置(附代码)
1 Maven依赖(版本号以最新稳定版为准)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2 配置分页插件(重要!不配报错)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 指定数据库类型为MySQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
关键点:如果不配置这个拦截器,Page对象只会查询全部数据然后内存截取,等于没分页。
3 实体类与Mapper
// 实体类省略getter/setter
@Data
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
private Integer status;
private Date createTime;
}
public interface OrderMapper extends BaseMapper<Order> {
// 自定义分页方法(后面会讲)
IPage<Order> selectOrderPage(Page<?> page, @Param("status") Integer status);
}
三种分页写法对比:传统 vs 内置 vs 自定义
1 传统方式(自己写Limit)——不推荐
List<Order> list = orderMapper.selectList(
new QueryWrapper<Order>().last("LIMIT " + offset + ", " + size));
问题:SQL注入风险、数据库兼容性差、需要手工计算offset。
2 MyBatis-Plus内置Page(最常用)
// 查询第2页,每页10条
Page<Order> page = new Page<>(2, 10);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getStatus, 1)
.orderByDesc(Order::getCreateTime);
IPage<Order> result = orderMapper.selectPage(page, wrapper);
System.out.println("总记录数:" + result.getTotal());
System.out.println("当前页数据:" + result.getRecords());
3 自定义SQL分页(适合多表联查)
<!-- OrderMapper.xml -->
<select id="selectOrderPage" resultType="com.example.entity.Order">
SELECT o.*, u.name AS userName
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.status = #{status}
</select>
// Mapper接口方法
IPage<Order> selectOrderPage(Page<?> page, @Param("status") Integer status);
// 调用(注意:Page参数必须放在第一个位置)
Page<Order> page = new Page<>(1, 10);
IPage<Order> result = orderMapper.selectOrderPage(page, 1);
底层原理:MyBatis-Plus会自动改写SQL,在原SQL尾部追加LIMIT,并执行一次COUNT查询。
高频面试题:分页SQL的常见坑(问与答)
Q1:为什么我的分页查询很慢,明明加了索引?
答:检查你用的LIMIT offset, size,当offset很大时(比如第100万条),MySQL依然要扫描前100万行,解决方案是延迟关联或子查询:
-- 传统写法
SELECT * FROM orders WHERE status=1 LIMIT 100000, 20;
-- 优化写法(先走覆盖索引获取ID,再回表)
SELECT * FROM orders
WHERE status=1 AND id > (
SELECT id FROM orders WHERE status=1 ORDER BY id LIMIT 100000, 1
) LIMIT 20;
Q2:MyBatis-Plus分页和手写Limit有什么区别?
答:MyBatis-Plus通过PaginationInnerInterceptor拦截器,自动生成COUNT和LIMIT语句。核心优势:
- 自动处理不同数据库方言(MySQL用LIMIT,Oracle用ROWNUM)
- 返回的
IPage对象自带total、pages等分页元数据 - 不用手动拼接
COUNT查询
Q3:分页时COUNT查询性能差怎么办?
答:如果表数据量极大,COUNT全表扫描很耗时,可考虑:
// 配置不走COUNT,返回的total为0(需自行判断是否需要总页数) page.setSearchCount(false);
或者用SELECT COUNT(*)改为SELECT COUNT(id)(走索引更快)。
性能调优:百万级数据分页不卡顿的5个技巧
1 强制走覆盖索引
在WHERE和ORDER BY字段上建立联合索引,避免filesort。
2 避免“深分页”
产品上限制最大页码(例如只能翻到第1000页),超过则提示用“加载更多”或“日期区间查询”。
3 使用游标分页(Keyset Pagination)
适用于实时性高的场景(如用户动态流),不依赖页码,只记录上次最后一条的ID:
WHERE id < 1000 ORDER BY id DESC LIMIT 20
4 开启MyBatis-Plus的分页插件缓存
在插件中设置optimizeJoin为true,可优化COUNT语句的JOIN。
5 必要时不用COUNT
业务上如果展示“加载更多”,无需返回总条数,用page.setSearchCount(false)减少一次查询。
什么时候该用物理分页,什么时候该用逻辑分页
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 后台管理系统表格 | 物理分页(MyBatis-Plus) | 数据量大,需精确页码 |
| 移动端“上拉加载” | 游标分页 | 避免深分页,体验好 |
| 数据量<1000条 | 逻辑分页(一次查全部分页) | 减少连接交互,简单快捷 |
最后提醒:分页不是简单的“查一页数据”,而是数据库IO、网络传输、业务交互的综合权衡,用MyBatis-Plus,一定要配好拦截器,理解COUNT的代价,并在SQL层面做索引优化。
(文中所有代码基于MyBatis-Plus 3.5.x版本,不同版本API略有差异,请以官方文档为准。)