MySQL高效分页-mybatis插件PageHelper改进

参考https://blog.csdn.net/weixin_36666151/article/details/80471767
mysql分页,首先想到的是limit,其实是比较简单的,那为什么还要用插件呢。limit某些情况下会执行全表扫描,针对大数据表时性能会很差,下面我们看看如何优化。

Executor

Mybatis中所有的Mapper语句的执行都是通过Executor进行的,Executor是Mybatis的一个核心接口,其定义如下。从其定义的接口方法我们可以看出,对应的增删改语句是通过Executor接口的update方法进行的,查询是通过query方法进行的。

PageHelper性能分析

MySQL分页在表比较大的时候,分页就会出现性能问题,MySQL的分页逻辑如下:比如:select * from user limit 100000,10

它是先执行 select * from user 扫描满足这个SQL语句,拿到执行结果后, 一页一页的找到行号为100000的行,返回接下来的10行数据,出现性能问题的原因有两个:

  1. 它先全表扫描了,整个表,而不是扫描到了满足条件的数据就不扫描了,比如select * from user limit 1,10 这个,它不是扫描到满足条件的10行数据就完事了,而是扫描了整个表,然后从这个结果集中从上往下扫描,只到找到行号为1的后面10行数据,这里出现性能问题的原因
    注意:查询条件字段没有索引会导致全表扫描
  2. 就在于MySQL寻找行号的逻辑是怎么寻找的,是不是像如果是像数组那样通过下标一步定位行号就不存在页码大小的问题了,但是MySQL不是一步到位的找到这个页码的,具体是怎么找到页码的感兴趣的可以去看MySQL的源码,我们能做的就是将MySQL的逻辑转换为直接定位数据的位置。

比如Mybatis 上的SQL语句为:


mybatis的 PageHelper 插件会在上面直接加上 limit 语句,源码如下:

public class MySqlDialect extends AbstractHelperDialect {
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(sql);
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getPageSize());
        } else{
            sqlBuilder.append(sql);
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow()); 
        }
        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }

就是直接调用MySQL的分页limit函数。

性能扩展

如何mybatis的PageHelper插件能将我们的SQL语句改成如下,那就大大提高大表的翻页查询效率,我本人亲七万行数据的表分页到最后一页这种方式比直接limit的方式快10倍,更大的表效率更大,其实原理很简单,我们给查询结果集加一个行号,查询出ID,和行号,再和原表通过ID关联,因为关联走了索引,索引速度很快,然后直接通过行号定位数据,速度大大提高

select id, name from 
(Select id as id2,(@rowNum:=@rowNum+1) as rowNo From user,(Select (@rowNum :=0) ) b) r ,user t 
where r.id2= t.id and r.rowNo> 100000  and t.name like '%小明%' order by id desc LIMIT 10

我们来修改mybatis的源码:其实非常简单。很多人可能mybaits的分页插件都没用过,我这里也将介绍其全部使用过程。
在pom.xml中引入:

  
        
            com.github.pagehelper
            pagehelper
            5.0.0
        
        
            com.github.pagehelper
            pagehelper-spring-boot-autoconfigure
            1.2.7
        
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
            1.2.7
        

接下来就直接使用就行了比如在controller中直接使用

    @RequestMapping(value = "/")
    @ResponseBody
    public Object say(HttpServletRequest request) {
        PageHelper.startPage(2, 4);//分页查询
        ...
        PageInfo pageInfo = new PageInfo<>(list);//分页结果
        return list;
    }
 
 

接下来我们来修改mybatis分页插件的拼接limit语句的逻辑代码,方法非常简单,新建一个这样的类,下面的的代码全部不要改,包名,类名都不能改。其目的就是利用Java类加载机制,替代其原来jar包里面有的这个对象,因为这个对象已经存在了,Java就不会再去加载其原来插件里面的这个对象了,从而巧妙的修改了其源码。

package com.github.pagehelper.dialect.helper;
import org.apache.ibatis.cache.CacheKey;
import com.github.pagehelper.Page;
import com.github.pagehelper.dialect.AbstractHelperDialect;

public class MySqlDialect extends AbstractHelperDialect {
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sql= sql.toLowerCase();//全部转换成小写形式
        if (page.getStartRow() == 0) {
            sqlBuilder.append(sql);
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getPageSize());
        } 
        else if(page.getStartRow()>10000&&this.inSingletonTable(sql)){//判断是否是大页码并且单表查询
            String[] tables = this.getTableName(sql);
            String sql1 =sql.split(tables[0])[0];
            sqlBuilder.append(sql1);
            sqlBuilder.append(" (Select id as id2,(@rowNum:=@rowNum+1) as rowNo From ");
            sqlBuilder.append(tables[0]);
            sqlBuilder.append(",(Select (@rowNum :=0) ) b) r ,");
            sqlBuilder.append(tables[0]);
            sqlBuilder.append(" ");
            sqlBuilder.append(tables[1]!=null?tables[1]:" ");
            sqlBuilder.append(" where r.id2= ");
            sqlBuilder.append(tables[1]!=null?tables[1]:tables[0]);
            sqlBuilder.append(".id ");
            sqlBuilder.append(" and r.rowNo> ");
            sqlBuilder.append(page.getStartRow());
            
            if (sql.contains("where")) {//拼接原来SQL语句中的where语句后面的语句
                sqlBuilder.append(" and ");
                sqlBuilder.append(sql.split("where")[1]);
                }else {
                    //拼接原有的SQL表名后面的一段后面
                    if (tables[1]!=null) {//表有别名
                    String[] sql2 =sql.split(tables[1]);
                    sqlBuilder.append(" ");
                    sqlBuilder.append(sql2.length>1?sql2[1]:" ");
            }else {
                String[] sql2 =sql.split(tables[0]);
                sqlBuilder.append(" ");
                    sqlBuilder.append(sql2.length>1?sql2[1]:" ");
            }
         }
             sqlBuilder.append(" LIMIT ");
             sqlBuilder.append(page.getPageSize());
        }else{
            sqlBuilder.append(sql);
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow()); 
        }
        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }
    
    private boolean inSingletonTable(String sql) {
        if (sql.contains("join")||sql.contains("JOIN")) {
         return false;
     }
        
        if (sql.contains("where")) {
             if (sql.contains("from")) {
                String tables= sql.split("from")[1].split("where")[0];
                if (tables.contains(",")) {
                    return false;
             }
         }
     }
     return true;
 }
private String[] getTableName(String sql) {
    String[] tables = new String[2];
    if (sql.contains("where")) {
        String tablenames = sql.split("from")[1].split("where")[0];
        tablenames = this.removekg(tablenames);//删除表名前后的空格
        if (tablenames.contains(" ")) {
            tables=tablenames.split(" ");
            return tables;
        }else {
            tables[0]=tablenames;
            return tables;
        }
    } else if (sql.contains("group")&&!sql.contains("order")) {
                String tablenames = sql.split("from")[1].split("group")[0];
        tablenames = this.removekg(tablenames);//删除表名前后的空格
        if (tablenames.contains(" ")) {
            tables=tablenames.split(" ");
            return tables;
        }else {
            tables[0]=tablenames;
            return tables;
        }
    } else if (sql.contains("order")&&!sql.contains("group")) {
         String tablenames = sql.split("from")[1].split("order")[0];
         tablenames = this.removekg(tablenames);//删除表名前后的空格
         if (tablenames.contains(" ")) {
             tables=tablenames.split(" ");
             return tables;
         }else {
            tables[0]=tablenames;
            return tables;
        }
    } else if (sql.contains("order")&&sql.contains("group")) {
         int orderIndex =sql.indexOf("order");
         int groupIndex =sql.indexOf("group");
         if (orderIndex

逻辑就这样简单。这里我是给他加了一个分支逻辑,当页码很大的时候,并且是单表查询的时候执行我这个分页SQL的拼接逻辑,多表关联的以后我想到好方法了再帖出来。

你可能感兴趣的:(MySQL高效分页-mybatis插件PageHelper改进)