• 已删除用户
Administrator
发布于 2022-09-15 / 1 阅读
0

PageHelper源码解读—Count语句优化

我们在使用 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

参考资料

PageHelper 更新日志 5.0.4 - 2017-08-01