詳解分頁元件中查count總記錄優化
阿新 • • 發佈:2020-03-18
# 1 背景
研究mybatis-plus(以下簡稱MBP),使用其分頁功能時。發現了一個[JsqlParserCountOptimize](https://gitee.com/-/ide/project/baomidou/mybatis-plus/edit/master/-/mybatis-plus-extension/src/main/java/com/baomidou/mybatisplus/extension/plugins/pagination/optimize/JsqlParserCountOptimize.java)的分頁優化處理類,官方對其未做詳細介紹,網上也未找到分析該類邏輯的隻言片語,這情況咱也不敢用呀,索性深度剖析一下,也方便他人。
# 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優化原始碼大致如下,對應上面的優化和限制