1 背景

研究mybatis-plus(以下简称MBP),使用其分页功能时。发现了一个JsqlParserCountOptimize的分页优化处理类,官方对其未做详细介绍,网上也未找到分析该类逻辑的只言片语,这情况咱也不敢用呀,索性深度剖析一下,也方便他人。

2 原理

首先PaginationInterceptor分页拦截器的原理这里不累述(mybatis通用分页封装的实现原理挺简单的,也就那么回事),最终落实到查询上基本是分为2个sql:查count总记录数 + 查真实分页记录。而此类是用优化来其中的查count这步。这count查询要怎么优化?这里上真实场景帮助大家理解: 假如有2张表user、user_address、user_account分别记录用户和用户地址和用户账户,1个用户可能有多个地址即1对多关系;1个用户只能有1个账户即1对1关系。

2.1 优化order by

先看下面的sql,放到分页查询下

select * from user order by age desc, update_time desc

传统分页组件往往是

查count:
select count(1) from (select * from user order by age desc, update_time desc)
查记录:
select * from user order by age desc, update_time desc limit 0,50

发现问题了吗?查count时的order by是完全可以去掉的!在复杂查询、大表、非索引字段排序等情况下查记录已经很慢了,查count又要来一次!所以查count显然希望优化为select count(1) from (select * from user)

2.1.1 限制

但是也不是所有场景都可以优化的,比如带group by的查询

2.1.2 源码

所以MBP源码如下实现,没有group by且有order by的语句,就把order by去掉

// 添加包含groupBy 不去除orderBy
if (null == groupBy && CollectionUtils.isNotEmpty(orderBy)) {
plainSelect.setOrderByElements(null);
sqlInfo.setOrderBy(false);
}

2.2 优化join场景

在join操作时,也存在优化可能,看下面sql

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid

这时候分页查count时,其实可以去掉left join直查user,因为user与user_account是1对1关系,如下

查count:
select count(1) from user u
查记录:
select u.id,ua.account from user u left join user_account ua on u.id=ua.uid limit 0,50

2.2.1 限制

查count能否去掉join直查首表,还存在诸多限制,如下:

表记录join后不能放大记录数

从上面案例可知,如果left join后记录数对比直查首表的总记录数会放大,就不能进行这个优化。比如3个用户每人各记录2条地址

select u.id,ua.address from user u left join user_address ua on u.id=ua.uid (6条)
vs
select count(1) from user u (3条)

此时去掉left join去查count就会得到更少的总记录数。注意这可能会变成一个坑,MBP无法自动判断本次分页查询是否会进行记录放大,所以join优化默认是关闭的,如果想开启需要声明自定义的JsqlParserCountOptimize bean,并设置optimizeJoin为true,如下

 @Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}

其实这里源码设计有些不合理,因为开了之后就得小心翼翼的审查自己各类left join的分页代码了,如果有放大的话,只能构造Page对象时,设置optimizeCountSql为false(默认true),相当于关闭本次查询所有count优化,那么不光是join,包括order by等优化也都不进行了。建议可以改为从Page(或ThreadLocal?)中获取optimizeJoin,变为每次查询级别可配的配置,默认关,而经过开发人员确认可join优化的才主动在本次查询级别设置开启。

仅限left join

如果是inner join或right join往往都会放大记录数,所以MBP优化会自动判断如果多个join里出现任何非left join的,就不进行此优化,比如from a left join b .... right join c... left join d此时会直接不进行优化

on语句有查询条件

比如

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid and ua.account > ?

where语句包含连接表的条件

比如

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid where ua.account > ?

2.2.2 源码

MBP的join优化源码大致如下,对应上面的优化和限制

List<Join> joins = plainSelect.getJoins();
// 是否全局开启了optimizeJoin(这里建议还可以从Page中按每次查询设置)
if (optimizeJoin && CollectionUtils.isNotEmpty(joins)) {
boolean canRemoveJoin = true;
String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY);
for (Join join : joins) {
// 仅限left join
if (!join.isLeft()) {
canRemoveJoin = false;
break;
}
Table table = (Table) join.getRightItem();
String str = Optional.ofNullable(table.getAlias()).map(Alias::getName).orElse(table.getName()) + StringPool.DOT;
String onExpressionS = join.getOnExpression().toString();
/* 如果 join 里包含 ?(代表on语句有查询条件)
或者
where语句包含连接表的条件
就不移除 join */
if (onExpressionS.contains(StringPool.QUESTION_MARK) || whereS.contains(str)) {
canRemoveJoin = false;
break;
}
}
if (canRemoveJoin) {
plainSelect.setJoins(null);
}
}

2.3 优化select count(1)位置

传统的分页,往往是在原始查询sql的外层套select count(1),比如

select count(1) from (select * from user)

而count真实目的是得到记录数,完全不需要原始查询里的select *产生额外耗时,所以可以优化为如下语句

select count(1) from user

2.3.1 限制

同样的,有一些场景不能进行count位置优化

select的字段里包含参数

如果select中包含#{}、${}等待替换的参数,也不能进行此优化,因为后续占位符替换真实值阶段会由于占位符个数减少导致报错,比如

select count(1) from (select power(#{aSelectParam},2) from user_account where uid=#{uidParam}) ua
vs
select count(1) from user_account where uid=#{uidParam} ua

MBP官方issue#95登记了此问题

包含distinct

select中包含distinct去重的语句,若去除有可能导致count记录数增大,所以不能进行此优化。比如

select count(1) from (select distinct(uid) from user_address) ua
vs
select count(1) from user_address ua #记录数可能增大

包含group by

包含group by的语句,由于select中往往会有聚合函数,所以count(1)内置语义变成了聚合函数,不能进行此优化。比如

select count(1) from (select uid,count(1) from user_address group by uid) ua #返回单行单列总记录数
vs
select count(1) from user_address group by uid #返回多行单列聚合count数

2.3.2 源码

MBP中相关源码如下

//select的字段里包含参数不优化
for (SelectItem item : plainSelect.getSelectItems()) {
if (item.toString().contains(StringPool.QUESTION_MARK)) {
return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
}
}
// 包含 distinct、groupBy不优化
if (distinct != null || null != groupBy) {
return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
}
...
// 优化 SQL,COUNT_SELECT_ITEM其实就是select count(1)语句
plainSelect.setSelectItems(COUNT_SELECT_ITEM);

3 总结

本文其实是针对通用分页组件中,对查count记录数这一步骤的一些优化思路,回顾一下:

  • 优化order by
  • 优化join语句
  • 优化select count(1)位置
  • 注意以上优化对应的限制,否则可能导致业务错误(特别是join优化,比较隐藏)

其实并不局限于MBP,大家自定义的分页拦截器也可以尝试用上,对分页时的优化还是效果显著的

“用来记录生命的演进,故事的迭代。期望做一个给大家带来帮助和思考的平台” ——深邃老夏

详解分页组件中查count总记录优化的更多相关文章

  1. 详解angular2组件中的变化检测机制(对比angular1的脏检测)

    组件和变化检测器 如你所知,Angular 2 应用程序是一颗组件树,而每个组件都有自己的变化检测器,这意味着应用程序也是一颗变化检测器树.顺便说一句,你可能会想.是由谁来生成变化检测器?这是个好问题 ...

  2. Unity Jobsystem 详解实体组件系统ECS

    原文摘选自Unity Jobsystem 详解实体组件系统ECS 简介 随着ECS的加入,Unity基本上改变了软件开发方面的大部分方法.ECS的加入预示着OOP方法的结束.随着实体组件系统ECS的到 ...

  3. [转]详解C#组件开发的来龙去脉

    C#组件开发首先要了解组件的功能,以及组件为什么会存在.在Visual Studio .NET环境下,将会有新形式的C#组件开发. 组件的功能 微软即将发布的 Visual Studio .NET 将 ...

  4. Angular6 学习笔记——组件详解之组件通讯

    angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...

  5. zz详解深度学习中的Normalization,BN/LN/WN

    详解深度学习中的Normalization,BN/LN/WN 讲得是相当之透彻清晰了 深度神经网络模型训练之难众所周知,其中一个重要的现象就是 Internal Covariate Shift. Ba ...

  6. 详解 $_SERVER 函数中QUERY_STRING和REQUEST_URI区别

    详解 $_SERVER 函数中QUERY_STRING和REQUEST_URI区别 http://blog.sina.com.cn/s/blog_686999de0100jgda.html   实例: ...

  7. 详解 Go 语言中的 time.Duration 类型

    swardsman详解 Go 语言中的 time.Duration 类型swardsman · 2018-03-17 23:10:54 · 5448 次点击 · 预计阅读时间 5 分钟 · 31分钟之 ...

  8. 详解jquery插件中(function ( $, window, document, undefined )的作用。

    1.(function(window,undefined){})(window); Q:(function(window,undefined){})(window);中为什么要将window和unde ...

  9. [转载]详解网络传输中的三张表,MAC地址表、ARP缓存表以及路由表

    [转载]详解网络传输中的三张表,MAC地址表.ARP缓存表以及路由表 虽然学过了计算机网络,但是这部分还是有点乱.正好在网上看到了一篇文章,讲的很透彻,转载过来康康. 本文出自 "邓奇的Bl ...

随机推荐

  1. Qt5 提示:无法启动此程序,计算机丢失Qt5Widgetsd.dll的解决方法

    Qt5工程编译生成可执行的exe文件之后,运行提示无法启动此程序,计算机丢失Qt5Widgetsd.dll… 原因是没有设置好Qt5的环境变量,解决方法如下: 1.打开[环境变量],(不同的系统会有不 ...

  2. 基于Docker 部署Jmeter + Grafana + InfluxDB 性能测试监控配置(亲测可用)

    工具介绍: InfluxDB:是一款用Go语言编写的开源分布式时序.事件和指标数据库,无需外部依赖.该数据库现在主要用于存储涉及大量的时间戳数据,如DevOps监控数据,APP metrics, lo ...

  3. php--ip的处理

    1.获取ip /**获取请求ip**/ function _get_request_ip(){ //strcasecmp 比较两个字符,不区分大小写.返回0,>0,<0. if(geten ...

  4. Android Studio那些错误的问题们

    本片博客会记录关于Android开发工具Android Studio出错的那些问题,包括导入项目编译失败.时间过长,莫名其妙的歇菜等等... 问题 3facets cannot be loaded.Y ...

  5. Ionic3学习笔记(八)使iOS端、Android端 Navbar 透明化

    本文为原创文章,转载请标明出处 若要使iOS端 Navbar 透明,只需要给 ion-navbar 添加 transparent 属性,但是Android端依旧会有一条border在,所以再给 ion ...

  6. derby常用语法

    derby常用语法 以user表为例: 1.创建表 create table user (id int primary key,account varchar(5),name varchar(5),p ...

  7. 用了python多进程,我跑程序花费的时间缩短了4倍

    应用场景:本人需要对200万条网页html格式数据进行清洗,提取文字后将分词结果写入数据库,之前做了一次,大概花费了80多个小时才跑完.机器配置是4核,内存8G:开完会领导让再改点东西重新跑一遍,然后 ...

  8. Proto3:C++基本使用

    本教程提供protocol buffer在C++程序中的基础用法.通过创建一个简单的示例程序,向你展示如何: 在.proto中定义消息格式 使用protocol buffer编译器 使用C++ pro ...

  9. 修改 Cucumber HTML 报告

    后台服务是 JSON-RPC 风格的,所以 Scenario 都是这样的 Scenario: login successful When I set request body from "f ...

  10. Babel 配置用法解析

    Babel 配置用法解析 刚复工的时候我司业务太多了,我已不记得我们连续作战了多少天,最近算是有时间可以学习学习我的babel大宝贝了,上周末看了下babel的一些核心模块以及babel的一些配置,今 ...