[Querydsl] Pagenation Count 쿼리 조건에 따라 호출하지 않도록 하여 성능 최적화

박철현·2024년 10월 18일
0

스프링부트

목록 보기
5/8

Page 알고 보자

  • content : 페이지 적용 쿼리 결과
    • Querydsl의 fetch(), fetchResult() 결과값
    • fetchResult()의 경우 deprecated로 권장되지 않음
  • total : 페이징을 적용하지 않은 전체 결과 크기
    • content와 동일한 where절 사용
  • offset : 현재 페이지 시작 인덱스
    • ex) 페이지 크기 10, 현재 2페이지 => offset : 20

PageableExecutionUtils

  • PageImpl 객체 생성 시 cout 쿼리를 개선한 추상 클래스
  • getPage()라는 단 하나의 정적 메서드만을 가지며 아래 2가지의 경우 count SQL 쿼리를 호출하지 않고도 전체 total을 구하기 때문에 성능 최적화
    • 첫번째 페이지 이면서 content의 크기가 한 page size 보다 작을 경우

      • content : 3개 / page 크기 : 10
      • 이 경우는 전체 개수가 3개로 반환하면 되기에 SQL Count 쿼리 무의미
        • content.size() 호출

    • 마지막 페이지 일때 Count 쿼리를 날리지 않고도 Offset과 content 개수로 전체 개수 파악 가능

      • 마지막 페이지 : offset이 0이 아니면서 content의 크기가 한 page의 크기보다 작은 경우
      • offset + content로 total 값 대신 사용 가능 하므로 SQL Count 쿼리 발생X

성능 최적화

기존 방식

// 전체 데이터 개수 추출
Long total = jpaQueryFactory.select(expenditure.count())
	.from(expenditure)
	.where(
		expenditure.member.eq(member),
		expenditure.spendDate.between(startDate, endDate),
		builder // 조건 동적 추가
	)
	.fetchOne();

	return new PageImpl<>(expenditures, pageable, total != null ? total : 0L);
  • fetchOne()으로 항상 count 쿼리 실행한 뒤 PageImpl 객체 생성

개선 방식

// 전체 데이터 개수 추출
JPAQuery<Long> total = jpaQueryFactory.select(expenditure.count())
			.from(expenditure)
			.where(
				expenditure.member.eq(member),
				expenditure.spendDate.between(startDate, endDate),
				builder // 조건 동적 추가
			);

		return PageableExecutionUtils.getPage(expenditures, pageable, total::fetchOne);
  • PageableExecutionUtils 추상 클래스를 활용하여 첫 페이지, 마지막 페이지에서 count 쿼리 발생하지 않도록 하여 성능 개선

단 Count Query에서 Group By 사용 시 사용할 수 없다.

  • Count 쿼리 작성하는 부분에서 groupBy 메서드를 사용한 경우 실제 DB에서 쿼리 결과가 결과가 여러 row로 나타남
  • 위에서 언급한 fetchOne 람다 메서드를 실행시키면 해당 페이지만큼의 사이즈만 가져와서 무조건 1페이지만 가져옴
    • 아래 이미지 결과 예시에서, fetchOne 하면 결국 limit1
    • 그러면 무조건 1 로서 개수 적용시키려함 -> 실제 데이터 수와 관계 없이 무조건 1페이지만 존재
    • 왜냐면 결과에서 1개만 가져오기 때문에 1행의 값인 "1" 한개만 가져와서 무조건 1개로 인식

Group By 예시 1

Group By 예시 2

CREATE TABLE orders (
                        country VARCHAR(10),
                        status  VARCHAR(10)
);

INSERT INTO orders VALUES
                       ('KR', 'NEW'),
                       ('KR', 'NEW'),
                       ('KR', 'DONE'),
                       ('US', 'NEW'),
                       ('US', 'DONE'),
                       ('US', 'DONE');


SELECT country, COUNT(*) AS cnt
FROM orders
GROUP BY country;

  • 이 이미지는 3으로 가져오겠으나, 결국 총 6개로 가져왔어야했는데 3개만 가져온것

문제 상황 1의 해결

  • 위에서 언급한 방식 : total::fetchOne 메서드 실행 결과 처럼 count 쿼리의 결과 (단일행으로 전체 데이터 수)를 따라 하기 위해 아래의 방식으로 변경
    • count 쿼리를 fetch() 메서드로 실행시키고, 해당 리스트의 size()메서드를 넘겨서 전체 데이터 수를 넘겨줘야 한다.
      • count 쿼리를 fetch() 실행 : List<Long> list = [1, 1, 1, 1, ...., 1] 형태의 리스트 반환
      • list.size() 인 총 1의 개수로 해결

적용 코드

// 전체 데이터 개수 추출
JPAQuery<Long> total = jpaQueryFactory.select(expenditure.id, expenditure.count())
			.from(expenditure)
			.where(
				expenditure.member.eq(member),
				expenditure.spendDate.between(startDate, endDate),
				builder // 조건 동적 추가
			)
            .groupBy(expenditure.id);

// 기존 : return PageableExecutionUtils.getPage(content, pageable, total::fetchOne);
        return PageableExecutionUtils.getPage(content, pageable, () -> total.fetch().size());
  • id별로 그룹 개수를 구하기 위한 쿼리로 위 이미지와 같이 여러 row로 출력
  • 똑같이 최적화가 적용된다.

위에서 예시 2번은 어떻게하지?

  • 1로만 이뤄진 데이터가 아닌 각 row가 다른 개수를 나타낸다면?
  • 결과는 간단하다 모든 행을 더해버리면 그게 결국 총 개수니깐 더해버리기~!
// 실제 페이징엔 getPage(content, pageable, supplier) 사용
return PageableExecutionUtils.getPage(content, pageable, () -> {
    List<Tuple> rows = grouped.fetch();
    // Tuple.get(1, Long.class) 에서 1은 select 열 순서(country=0, count=1)
    return rows.stream()
               .mapToLong(t -> t.get(1, Long.class))
               .sum();
});

이런느낌으로 size()메서드가 아닌 각 리스트를 순회하면서 총 개수를 더해서 넘겨줘야함!

출처

profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글