我们在使用 PageHelper 进行大表关联查询分页列表时,经常出现查询分页数据并未执行很久,但自动 count 开销非常大。这是因为 PageHelper 的分页拦截器在计算总记录数时,是在原语句上嵌套一层,比如:
SELECT count(1) FROM (主SQL)
这个主 SQL 是没有 limit 的,执行计划会扫描大量的数据行,容易导致 CPU 异常。
最先遇到这这种场景,我们一般是弃用了 PageHelper,手写分页与求总记录数的方法,通过优化 count 的方式来达到 SQL 执行效率优化。其实在5.0.4 - 2017-08-01
版本中,PageHelper 就增加手写 count 查询支持。
源码摘要
PageInterceptor.java
private String countSuffix = "_COUNT";
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
...... // 省略若干行
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射获取动态参数
String msId = ms.getId();
Configuration configuration = ms.getConfiguration();
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
String countMsId = msId + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
if(countMs != null){
count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
//处理查询总数
//返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
...... // 省略若干行
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
源码解析
增加 countSuffix count
查询后缀配置参数,该参数是针对 PageInterceptor 配置的,默认值为 _COUNT。分页插件会优先通过当前查询的 msId + countSuffix 查找手写的分页查询。如果存在就使用手写的 count 查询,如果不存在,仍然使用之前的方式自动创建 count 查询。
关键代码
String countMsId = msId + countSuffix;
//先判断是否存在手写的 count 查询
MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
示例代码
<select id="selectLeftjoin" resultType="com.github.pagehelper.model.User">
select a.id,b.name,a.py from user a
left join user b on a.id = b.id
order by a.id
</select>
<select id="selectLeftjoin_COUNT" resultType="Long">
select count(distinct a.id) from user a
left join user b on a.id = b.id
</select>
上面的 countSuffix
使用的默认值 _COUNT
,分页插件会自动获取到 selectLeftjoin_COUNT
查询,这个查询需要自己保证结果数正确。
返回值的类型必须是resultType="Long"
,入参使用的和 selectLeftjoin
查询相同的参数,所以在 SQL 中要按照 selectLeftjoin
的入参来使用。
因为 selectLeftjoin_COUNT
方法是自动调用的,所以不需要在接口提供相应的方法,如果需要单独调用,也可以提供。
上面方法执行输出的部分日志如下:
DEBUG [main] - ==> Preparing: select count(distinct a.id) from user a left join user b on a.id = b.id
DEBUG [main] - ==> Parameters:
TRACE [main] - <== Columns: C1
TRACE [main] - <== Row: 183
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [com.github.pagehelper.mapper.CountryMapper]: 0.0
DEBUG [main] - ==> Preparing: select a.id,b.name,a.py from user a left join user b on a.id = b.id order by a.id LIMIT 10
DEBUG [main] - ==> Parameters:
TRACE [main] - <== Columns: ID, COUNTRYNAME, COUNTRYCODE
TRACE [main] - <== Row: 1, Angola, AO
TRACE [main] - <== Row: 2, Afghanistan, AF
TRACE [main] - <== Row: 3, Albania, AL