Mybatis-plus 分页查询的“坑”(不,是我不会用)

经常做列表界面性能优化的小伙伴都遇到过这样的情况,索引什么的都命中,但是性能就是不好,其中一种情况就是因为 列表为分页查询,列表查询本身挺快的,但是拿到分页的count语句在数据库中执行慢的一批。

count慢,最直接的解决办法我觉得就是不查询总条数,用户一般不会关心具体有多少页,更多时候进入界面响应要快,然后输入查询条件,过滤掉大部分数据。像百度,谷歌一样不需要展示具体页数和总条数,然而,上面我说的这些,没有打动产品经理,也没时间所有界面挨个修改,那目前就只能继续处理数量查询。

今天其他的情况先不说,先吐槽一下mybatis-plus (下面全部简写为mp)自带的分页工具的小坑。测试中偶尔会出现count 语句包裹整个sql的情况,比如

select count(*) from (

        select id, name, createTime, otherKeys  from tableA  a

                join tableR  rr on a.id = rr.a_id

                where a.is_deleted = 0 order by create_time desc

) as total

这个sql能正常运行,但是处于性能考虑,我们可以去掉 order,去掉select 中的字段

select count(*)  from tableA  a

                join tableR  rr on a.id = rr.a_id

                where a.is_deleted = 0

这样性能不是更好吗?

之前遇到这种情况时,代码中直接屏蔽count查询,再手写count语句

//方法一
Page page = new Page(1, 10);
page.setSearchCount(false);
//手写count语句


//方法二
//或者mapper中写一个同名查询以 _COUNT结尾,然后这样也行
page.setCountId("selectAll_COUNT");

题外话:

自定义count语句时,如果sq中的join太多,还大多只是拿来查询冗余数据,就可以尝试选择把一些不影响数量的left join 表给去掉,如果left join的表中的字段参与了界面上的查询,那就用  <if> </if> 标签判断,有这个查询条件时才参与 join。有些inner join 确保业务正常的情况下也能去掉,比如一定是一对一且一定不为空,只是作为查询展示字段不参与其他表的join,这样的 inner join 根据业务具体情况看是否也能去掉。

这样的好处是:大部分正常用户进来都是输入搜索条件查询,首页的数据就要尽量的快。有了搜索条件后,能过滤掉大部分数据,这样再把表join进来对性能影响不大。

题外话结束。。。。

但是为什么mp有的时侯能正常处理count语句,有时候不行? 之前我一直觉得是因为sql太复杂,不太好重构,但是这次出问题的sql(比如上面举例的那种)已经足够简单,还是不行,依然出现上述count语句包裹整个原始sql的情况。

接下来就是翻了一下源码,想知道到底哪些情况下回出现这样的坑,一不小心有发现了框架的“bug“, bug我打了引号,后面解释。

我的问题版本是

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

mp的默认实现是:

PaginationInnerInterceptor,当mp判断count sql 无法优化时会调用这个方法拼接sql

然后整理下哪些情况会调用这个方法

按图中的标记,以下情况会不优化 count 语句

1:page对象中 optimizeCountSql 设置为false。(可以手动修改)自己设置的放弃优化

2:后面单独写。。。。

3:sql是用union 把多个表合并的不行

4:select 中存在  #{} ${} 参数的不优化,因为可能你的参数是一个函数,比如max 这样就只有一行数据,count语句会不正确

5:包含 distinct、groupBy不优化,这种不解释了

6:前面如果遇到了异常,这是作为兜底的情况

一个bug!如下

然后,根据第4个条件,我突然想到,如果我在sql中就真的直接写一个max函数,这个不属于其中任何一种不优化情况,回会不会有问题呢?于是直接把项目的一个sql复制一下,写了mapper做测试,具体代码就不贴出来了,很简单  select max(id) from table_name

分页查询的结果是:

看到了吧,total = 3,但是只有一个records !

本来我想去github提一个 issues, 界面都打开了,想了一下又觉得这不算一个真正的bug吧。因为,sql的select用了这样的函数,然后再去分页查询,这种本来就是不正确的使用方式,兼容了最好,报错提示开发者也行。不处理,也能接受吧,程序员自己使用错误在先。这就是为什么前面 bug 这个词要打引号的原因。

还没完,上面列的6个条件。其中第二个的作用是把sql字符串解析成java对象才,方便操作。

第6条写的异常情况,我自己遇到的也就是这里!我有一个sql,不符合上面任何一个不优化的规则,但是还是出了问题,我去掉了里面的join,where等,把它简化到极致如下:

select 1 from tableA rr

凭借直觉和理智,这个没有不优化的理由,我看到的sql应该是 select count(*) from tableA

然后,把这个sql放入条件2中的代码执行一下,

Select select = (Select) CCJSqlParserUtil.parse("select 1 from tableA rr");

然后就报错了!!报错后就执行了条件6,因此没有优化这个sql的count语句

我试了 根据错误提示,我把 rr 换成as 也会报错,其他的随便试了几个都没报错!所以我认为 rr 是被识别成了一个关键字吧。CCJSqlParserUtil 里面的代码很多字符串解析的,看起来很痛苦,如同汇编,哪位大佬梳理了具体原因麻烦教小弟一下啊。。。

最后,PageHelper 我没有测试,因为这个出了很多次问题,已经被我们禁用了,

1:如果开启的PageHelper 但是程序异常或者有判断条件跳过sql执行,导致PageHelper没有执行也没有清理,会导tomcat致线程池参数混,界面数据不对,遇到过两次,然后全局排查

2:我们项目自定义了一些sql拼装的拦截器,用PageHelper会导致有些情况下拼装的sql不正确,所以就放弃了PageHelper

<think>嗯,用户想了解MyBatisPlus中BaseMapper的selectPage方法的使用方法,包括示例和分页查询。首先,我需要回忆一下MyBatisPlus的相关知识。记得BaseMapper是MyBatisPlus提供的一个通用Mapper接口,里面有很多内置的方法,比如selectById、insert、update等等,其中分页查询的方法应该是selectPage。 用户提到了几个引用,其中引用[1]和[2]提到selectPage方法需要传入Page对象,返回的也是Page对象,里面包含分页信息。引用[3]和[4]则补充了分页的实现方式以及另一个方法selectMapsPage的区别。这些信息需要整合到回答中。 首先,我需要说明selectPage的基本用法。需要创建一个Page对象,指定当前页和每页大小,然后在Service或Mapper层调用这个方法。记得要配置分页插件,否则分页功能可能起作用。比如在配置类里添加PaginationInterceptor,现在可能已经更新为MybatisPlusInterceptor,需要添加分页的拦截器。 然后,代码示例是关键。用户可能需要看到如何创建Page对象,如何调用selectPage,以及如何处理返回的结果。示例应该包括实体类、Mapper接口的定义,以及Service或Controller中的调用方法。另外,要提到分页查询的条件构造,比如使用QueryWrapper来添加查询条件,比如.eq或.like等方法。 可能还需要注意返回的Page对象中包含的数据列表、总记录数、总页数等信息,用户可能需要访问这些属性来展示分页结果。同时,要提到selectMapsPage的同之处,即返回的是Map列表,适用于需要动态字段的情况。 用户的问题可能还涉及到常见问题,比如为什么要配置分页插件,或者分页生效的可能原因。此外,是否需要处理总页数,或者如何根据返回的Page对象进行前端的分页展示,这些可能都是用户关心的点。 需要确保回答结构清晰,分步骤说明,从配置到使用,再到示例代码,可能还要注意事项和常见问题。同时,根据用户提供的引用,正确标注引用来源,比如引用[1]和[2]提到selectPage的参数和返回值,引用[3]说明分页的实现方式,引用[4]提到selectMapsPage的区别。 最后,生成的相关问题要围绕MyBatisPlus的分页功能,比如性能优化、其他分页方法、自定义分页等,这样用户如果有进一步的问题,可以引导他们继续探索。</think>### MyBatisPlus BaseMapper SelectPage 方法使用详解 #### 一、核心用法说明 1. **分页功能依赖配置** 需在配置类中添加分页插件拦截器(例如使用 `MybatisPlusInterceptor`),否则分页功能无法生效: ```java @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } ``` 2. **方法参数与返回值** `selectPage` 方法接受两个参数: - `Page<T>`:封装分页参数(当前页码、每页数量)和返回的分页数据 - `Wrapper<T>`:查询条件构造器(可空) 返回值为 `Page<T>` 对象,包含数据列表、总记录数、总页数等信息[^2][^3]。 --- #### 二、完整使用示例 1. **实体类定义** ```java @Data public class User { private Long id; private String name; private Integer age; } ``` 2. **Mapper接口定义** ```java public interface UserMapper extends BaseMapper<User> { // 继承BaseMapper后自动包含selectPage方法 } ``` 3. **分页查询实现** ```java // 创建分页对象(第2页,每页10条) Page<User> page = new Page<>(2, 10); // 构建查询条件(可选) QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.like("name", "张").gt("age", 18); // 执行分页查询 Page<User> resultPage = userMapper.selectPage(page, wrapper); // 获取分页数据 List<User> records = resultPage.getRecords(); // 当前页数据 long total = resultPage.getTotal(); // 总记录数 long pages = resultPage.getPages(); // 总页数 ``` --- #### 三、关键特性说明 1. **自动统计总数** 分页查询默认会执行 `COUNT(1)` 语句统计总记录数,可通过 `page.setSearchCount(false)` 关闭[^3]。 2. **特殊返回类型** `selectMapsPage` 方法返回 `List<Map<String, Object>>`,适用于动态字段查询场景[^4]。 3. **性能优化建议** 当数据量较大时,建议通过重写 `page.setOptimizeCountSql(true)` 启用优化统计逻辑。 --- #### 四、常见问题排查 1. **分页生效** 检查是否漏配分页插件拦截器,或分页参数设置错误。 2. **总数统计慢** 大数据量场景下建议使用 `page.setSearchCount(false)` 关闭自动统计,改用自定义统计逻辑。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值