본문 바로가기
스프링 부트/A-ger

스프링 커버링 인덱스로 페이징 성능 개선하기

by illlilillil 2022. 2. 18.

지난 글 

2022.02.07 - [스프링 부트/A-ger] - 스프링 무한스크롤 페이징 검색 처리 및 동적쿼리 적용하기

 

지난 글에서 무한 스크롤 적용을 위해 slice와 booleanExpression으로 동적 쿼리를 적용했었습니다.

검색 기능은 아무래도 이용자들이 가장 많이 이용할 기능이기에 더 많은 성능 개선을 위해 커버링 인덱스를 적용하기로 했습니다.

 

일단적으로 조회 쿼리에서 성능 저하의 원인이 되는 것은 인덱스 검색 후 컬럼 값을 읽을 때입니다.

select 부분에서 데이터 블록에 접근하기 때문에 많은 성능 저하를 유발합니다.

 

그러나 커버링 인덱싱을 적용하게 되면 where, order by, offset, limit를 인덱싱으로 빠르게 Id만 찾아내 데이터 블록에 접근하기에 성능 향상에 도움이 됩니다.

구현 코드

QueryDsl을 이용해 커버링 인덱스 기능을 적용하겠습니다.

적용 전 

@Override
    public Slice<ProductThumbResponse> findSellProductsByAccountId(Long accountId, Pageable pageable) {
        JPAQuery<Product> productQuery = queryFactory
                .selectFrom(product)
                .where(product.account.accountId.eq(accountId))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize() + 1); //limit보다 한 개 더 들고온다.
        for (Sort.Order o : pageable.getSort()) {
            PathBuilder pathBuilder = new PathBuilder(product.getType(), product.getMetadata());
            productQuery.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty())));
        }
        List<ProductThumbResponse> content = new ArrayList<>(ProductThumbResponse.toProductListResponse(productQuery.fetch()));
        boolean hasNext = false;
        //마지막 페이지는 사이즈가 항상 작다.
        if (content.size() > pageable.getPageSize()) {
            content.remove(pageable.getPageSize());
            hasNext = true;
        }
        return new SliceImpl<>(content, pageable, hasNext);
    }

 

적용 후

1) select를 id만 넣고 커버링 인덱스를 활용해 조회합니다.

2) 빈 리스트면 아예 반환을 시킵니다.

3) 발생한 ids로 실제 엔티티를 조회합니다.

@Override
public Slice<ProductThumbResponse> findSellProductsByAccountId(Long accountId, Pageable pageable) {
    List<Long> ids = queryFactory
            .select(product.productId)
            .from(product)
            .where(product.account.accountId.eq(accountId))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize() + 1)
            .fetch();

    if (CollectionUtils.isEmpty(ids)) {
        return new SliceImpl<>(new ArrayList<>(), pageable, true);
    }

    JPAQuery<Product> productQuery = queryFactory
            .selectFrom(product)
            .where(product.productId.in(ids));

    for (Sort.Order o : pageable.getSort()) {
        PathBuilder pathBuilder = new PathBuilder(product.getType(), product.getMetadata());
        productQuery.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty())));
    }
    List<ProductThumbResponse> content = new ArrayList<>(ProductThumbResponse.toProductListResponse(productQuery.fetch()));
    boolean hasNext = false;
    //마지막 페이지는 사이즈가 항상 작다.
    if (content.size() > pageable.getPageSize()) {
        content.remove(pageable.getPageSize());
        hasNext = true;
    }
    return new SliceImpl<>(content, pageable, hasNext);
}

 

가장 중요한 성능 비교를 해보면 1억개의 row, 5개의 컬럼을 조회 쿼리를 날렸을때 적용 전 페이징에 비해 적용 후 페이징은 

약 50배정도 감소한 믿기지 않는 성능을 보여준다고 합니다.

 

 단점

데이터 조회 전에 인덱싱이 필요하기 때문에 많은 데이터를 조회할 땐 너무 많은 인덱스가 생성이 됩니다.

인덱스 또한 데이터이기에 성능 이슈가 발생할 수 있습니다. where, order by 등에 필요한 컬럼이 필요하기 떄문입니다.

페이지 번호가 뒤에 있을수록 NoOffset 보다 느립니다.

 

우리 프로젝트에는 동적 쿼리, 여러 정렬이 한 번에 들어가 NoOffset을 적용할 수 없었는데요,,

여러 조건이 필요하지 않은 경우 NoOffset 방식 또한 고려 사항에 넣어둬야 할 것 같습니다.

 

 

 

참고 자료

 

2. 페이징 성능 개선하기 - 커버링 인덱스 사용하기

2. 커버링 인덱스 사용하기 앞서 1번글 처럼 No Offset 방식으로 개선할 수 있다면 정말 좋겠지만, NoOffset 페이징을 사용할 수 없는 상황이라면 커버링 인덱스로 성능을 개선할 수 있습니다. 커버링

jojoldu.tistory.com

 

댓글