MySQL高效分頁-mybatis外掛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語句為
<select id="queryUserListLikeName" parameterType="java.lang.String" resultType="com.entity.user">
select<include refid="Base_Column_List" />
from user t
WHERE t.name LIKE '%${name}%'
order by id desc
</select>
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:[email protected]+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的分頁外掛都沒用過,我這裡也將其全部使用過程。
我用的springboot
在pom.xml中引入:
<!-- 分頁外掛pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 分頁外掛pagehelper -->
如果你的mybatis版本和它的不同可能會提示有的jar沒有啥的,我的用的是
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
接下來就直接使用就行了比如在controller中直接使用
@RequestMapping(value = "/")
@ResponseBody
public Object say(HttpServletRequest request) {
PageHelper.startPage(2, 4);//僅僅一行程式碼就搞定,沒有其他的地方要改了。其他程式碼原來怎麼寫還是怎麼寫
List<user> list = userMapper.queryUserListLikeName("小明");pagedata pagedata= new pagedata(list);//這個pagedata物件是我自己寫的,我嫌mybatis提供的太囉嗦,主要是 從list中拿到實際的物件。
return pagedata;
}
接下來我們來修改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;
/**
* @author yangjiangcai
* qq 1097657841
*/
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:[email protected]+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<groupIndex) {
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 {
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("where")&&!sql.contains("order")&&!sql.contains("group")) {
String tablenames = sql.split("from")[1];
tablenames = this.removekg(tablenames);//刪除表名前後的空格
if (tablenames.contains(" ")) {
tables=tablenames.split(" ");
return tables;
}else {
tables[0]=tablenames;
return tables;
}
}
return tables;
}
//刪除字串兩頭的空格
private String removekg(String textContent) {
textContent = textContent.trim();
while (textContent.startsWith(" ")) {//這裡判斷是不是全形空格
textContent = textContent.substring(1, textContent.length()).trim();
}
while (textContent.endsWith(" ")) {
textContent = textContent.substring(0, textContent.length() - 1).trim();
}
return textContent;
}
}
完了,邏輯就這樣簡單。這裡我是給他加了一個分支邏輯,當頁碼很大的時候,並且是單表查詢的時候執行我這個分頁SQL的拼接邏輯,多表關聯的以後我想到好方法了再帖出來。
前端收到的{"data":[{"id":1,"name":"小明55"}],"pageNum":1,"pageSize":4,"total":1,"pages":1}List<user> list = userMapper.queryUserListLikeName("小明");
//簡單封裝
pagedata pagedata= new pagedata(list);
return pagedata;
pagedata 物件的程式碼我也帖到下面來,你們完全可以不使用,想用的就用吧,這個物件是純Java寫的,不依賴任何依賴
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class pagedata implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
// Page{count=true, pageNum=1, pageSize=2, startRow=0, endRow=2, total=10, pages=5, reasonable=false, pageSizeZero=false}
private List data;
private int pageNum;
private int pageSize;
private int total;
private int pages;
public pagedata(List list) {
if (list instanceof com.github.pagehelper.Page) {
this.data=new ArrayList<>();
for (Object object : list) {
data.add(object);
}
String strs= list.toString();
this.pageNum=Integer.parseInt(getVluse(strs,"pageNum"));
this.pageSize= Integer.parseInt(getVluse(strs,"pageSize"));
this.total= Integer.parseInt(getVluse(strs,"total")) ;
this.pages= Integer.parseInt(getVluse(strs,"pages"));
}
}
public List getData() {
return data;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public int getTotal() {
return total;
}
public int getPages() {
return pages;
}
@Override
public String toString() {
return "pagedata [data=" + data + ", pageNum=" + pageNum + ", pageSize=" + pageSize
+ ", total=" + total + ", pages=" + pages + "]";
}
/*
* 直接從json字串中獲取對應key的value值
* */
public static String getVluse(String jsonStr,String key) {
//本方法大概耗時25納秒
char[] strs = jsonStr.toCharArray();
String result="";
for (int i = jsonStr.indexOf(key)+key.length()+1; i < strs.length; i++) {
if (strs[i]!=','&&strs[i]!='}') {
result+=strs[i];
}else {
return result;
}
}
return "";
}
}