MyBatis-Plus 페이지 플러그인의 COUNT 최적화 방법

페이지 처리 설정 구성

package com.example.demo.config;

import com.baomidou.mybatisplus.extension.plugins.MyBatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan(basePackages = "com.example.demo.mapper")
public class MyBatisConfig {

    // 페이지 처리 플러그인 등록
    @Bean
    public MyBatisPlusInterceptor configurePagination(){
        MyBatisPlusInterceptor interceptor = new MyBatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

페이지 쿼리 예시

컨트롤러 페이지 요청

 @PostMapping("/searchPage")
    public IPage<ExperimentData> getPageData(){
        Page pageInfo = new Page();
        pageInfo.setCurrent(1L);
        pageInfo.setSize(2L);
        IPage<ExperimentData> result = service.getData(pageInfo);
        return result;
    }

서비스 계층 로직

 @Override
    public IPage<ExperimentData> getData(Page pageInfo) {
        return mapper.queryData(pageInfo);
    }

마이바티스 매퍼 인터페이스

IPage<ExperimentData> queryData(IPage pageInfo);

SQL 매핑 파일

<select id="queryData" resultType="com.example.demo.model.ExperimentData">
    select * from experiment_table
</select>

원본 쿼리 실행 흐름

  1. 자동 생성된 COUNT 쿼리 실행: select count(*) from (select * from experiment_table)
  2. 결과가 0보다 클 경우 실제 데이터 조회 쿼리 실행

첫 번째 단계: select count(*) from (select * from experiment_table) 두 번째 단계: select * from experiment_table limit xxx

성능 문제 분석

select count(*) from (select * from experiment_table) 쿼리는 전체 컬럼을 스캔하게 되어 성능 저하를 유발합니다. 필요한 컬럼만 선택하거나 인덱스를 활용한 COUNT 쿼리가 최적입니다.

플러그인 소스 분석

public class PaginationInnerInterceptor implements InnerInterceptor {
   
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 페이지 객체 추출
        IPage<?> page = (IPage)ParameterUtils.findPage(parameter).orElse(null);
        
        if (page != null && page.getSize() > 0 && page.isCountEnabled()) {
            // 커스텀 COUNT 쿼리 확인
            MappedStatement countMs = this.buildCustomCountStatement(ms, page.getCustomCountMethod());
            BoundSql countSql;
            
            // 커스텀 쿼리가 존재할 경우 실행
            if (countMs != null) {
                countSql = countMs.getBoundSql(parameter);
            } 
            // 기본 쿼리 생성
            else {
                countMs = this.generateAutoCountStatement(ms);
                String countSqlStr = this.optimizeCountQuery(boundSql.getSql());
                MPBoundSql mpBoundSql = PluginUtils.createBoundSql(boundSql);
                countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
                PluginUtils.updateParameters(countSql, mpBoundSql.additionalParameters());
            }

            CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
            List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
            long total = 0L;
            if (!result.isEmpty()) {
                total = Long.parseLong(result.get(0).toString());
            }

            page.setTotal(total);
            return this.continueProcessing(page);
        } else {
            return true;
        }
    }

  ......
   
}

이 로직을 통해 커스텀 COUNT 쿼리 메서드를 지정하면 기본 쿼리 대신 사용할 수 있습니다.

최적화된 쿼리 구현

컨트롤러 로직은 동일합니다

서비스 계층 변경

 @Override
    public IPage<ExperimentData> getData(Page pageInfo) {
        // 커스텀 COUNT 메서드 지정
        pageInfo.setCustomCountMethod("countEntries");
        return mapper.queryData(pageInfo);
    }

매퍼 인터페이스 확장

IPage<ExperimentData> queryData(IPage pageInfo);
Long countEntries(IPage pageInfo);

SQL 매핑 파일 수정

<select id="queryData" resultType="com.example.demo.model.ExperimentData">
        select * from experiment_table
</select>

<select id="countEntries" resultType="java.lang.Long">
        SELECT COUNT(*) FROM experiment_table
</select>

최적화 후 쿼리 실행 과정

첫 번째 단계: SELECT COUNT(*) FROM experiment_table 두 번째 단계: select * from experiment_table limit xxx

커스텀 COUNT 쿼리를 통해 불필요한 데이터 스캔을 제거하여 성능 향상

태그: MyBatis-Plus pagination SQL optimization indexing java

6월 13일 20:52에 게시됨