- 회원 API를 구현하면서 이메일과 닉네임 중복체크 기능을 구현하는데
현재 이 데이터가 DB에 있는지 없는지를 체크해야했다- 그 과정에서 exists와 count 쿼리의 성능을 비교해보았고
exists의 성능이 더 좋음을 알게되었다
- 그 과정에서 exists와 count 쿼리의 성능을 비교해보았고
- 관련 내용을 학습하는 중 [우아콘2020] 수십억건에서 QUERYDSL 사용하기를 보았고 도움이 많이 되었다
그러나 현재는 3년후인만큼, querydsl의 수정된 부분을 발견할 수 있었다- QueryDSL에서 기본적으로 지원하는 exists는
count 쿼리 방식을 사용해 성능상의 이슈가 있어
Querydsl에서 limit을 사용하여 직접 구현한 exists 쿼리를 사용한 설명의 영상인데 QuerydslJpaPredicateExecutor.java
를 보니
exists 메서드에서 fetchCount 함수를 사용한다고 한 부분이
fetchFirst 함수를 사용하고 있는것을 알 수 있었다
QueryDSL 5.0부터 fetchCount가 deprecated 되면서 변경된듯 하다
하지만 여전히 sub query에서만 가능해 limit을 사용하여 쿼리를 최적화해 사용했다
- QueryDSL에서 기본적으로 지원하는 exists는
exists와 count
특정 조건을 만족하는 row가 있는지 체크 = 데이터가 있는지 없는지 체크
sql에서의 exists와 count
- exists : 조건 만족하는 처음 값 발견하면 -> 쿼리종료
select exists (select 1
from member
where email = 'email'
)
- count : 조건 만족하는 처음 값 발견해도 -> 끝까지 조건 체크
-> 전체 데이터를 다 조회하기 때문에 성능이 떨어질 수 밖에 없음 (특히 스캔 대상이 앞에 있을수록 더 심한 성능차이가 있음)
select count(*)
from member
where email = 'email'
2020 우아콘 영상
우아콘 영상에서 나왔던 문제점
JPQL에서의 exists
- JPQL에서 select의 exists(select exists 문법)를 지원하지 않음
- where의 exists만 지원하기 때문에
-> count 쿼리를 사용해서 데이터 존재 여부를 확인해야함
-> 성능 이슈가 따라옴
@Query("SELECT COUNT(m.email) > 0 FROM Member m WHERE m.email =:email") boolean existsUsingCount(@Param("email") String email);
- where의 exists만 지원하기 때문에
QueryDSL에서의 exists
sql의 exists를 사용하지 않고 count 사용해서 체크했었음(fetchCount 함수 사용) (과거)
= exists 쿼리가 아닌 count query 발생했었음
- 현재는 fetchFirst 함수를 사용하고 있음을 확인할 수 있었음
QuerydslJpaPredicateExecutor.java
의 exists()
- 그럼에도, QueryDSL은 from절 없이 쿼리 생성이 불가하기 때문에
= select exists 하고 하위에 select 쿼리를 만드는 방식이 공식적으로 지원 안됨- from 절을 붙여야 하는데, 그렇게 되면 exists로의 성능 개선을 볼 수 없음
- 아래의 예시는 컴파일 에러는 나지 않지만 실행 후에는 에러가 떠 사용 불가
public Boolean exist(String email) { return queryFactory.select(queryFactory .selectOne() .from(member) .where(member.email.eq(email)) .fetchAll().exists()) .fetchOne(); }
해결 :: limit 1로 개선하기
exists가 count 보다 성능이 좋은 이유 : 전체를 조회하지 않고 첫번째 결과만 확인하기 때문
=> 이 내용을 가지고 직접 구현해보자
=> limit 1로 1개만 조회해보고, 이 결과로 있는지 없는지 판단하기
public Boolean existsUsingLimit(String email) {
Integer result = queryFactory
.selectOne()
.from(member)
.where(member.email.eq(email))
.fetchFirst();
return result != null;
}
fetchFirst()
=limit(1).fetchOne()
- 조회결과가 없으면 0이 아닌 null 반환함
- 1개가 있는지 null인지 판단하기
sql의 exists와 동일한 성능효과를 볼 수 있었다
- count 쿼리와 limit으로 구현한 쿼리 비교
String email = "test75000@mail.com";
@BeforeEach
void insertData() {
for (int i = 0; i < 100000; i++) {
Member member = Member.builder()
.email("test" + i + "@mail.com")
.build();
memberRepository.save(member);
}
}
@AfterEach
void cleanData() {
memberRepository.deleteAll();
}
@Test
void sqlCount() {
long startTime = System.currentTimeMillis();
long endTime = 0;
if(memberRepository.existsUsingCount(email)) {
endTime = System.currentTimeMillis();
}
System.out.println("==============================================");
System.out.printf("sqlCount 수행시간 : %d\n", endTime - startTime);
System.out.println("==============================================");
}
@Test
void querydslCustomExists() {
long startTime = System.currentTimeMillis();
long endTime = 0;
if(memberRepository.existsUsingLimit(email)) {
endTime = System.currentTimeMillis();
}
System.out.println("==============================================");
System.out.printf("querydslCustomExists 수행시간 : %d\n", endTime - startTime);
System.out.println("==============================================");
}
Spring Data JPA 쿼리 메서드의 exists
쿼리 메서드도 내부에서 limit으로 쿼리 최적화를 하고 있음
고려한 방법들
- 쿼리 메서드
- QueryDSL에서 limit 1 사용
처음엔 쿼리 메서드로 사용했다 내부에서 limit으로 쿼리 최적화가 되어있으니까
그런데 현재 나는 Member Entity에서 Email을 embedded type으로 사용하고 있어서
쿼리 메서드를 사용하려면 Email 객체를 생성해서 조회해야 했다
- 전
- Repository layer
boolean existsByEmail(Email email);
- Service layer
private boolean checkEmailDuplicate(Email email) { return memberRepository.existsByEmail(email); }
불필요한 객체 생성을 피하고자 결론적으로 QueryDSL에서 limit을 사용해 쿼리를 최적화했다
- 후
- Repository layer
public boolean existsByEmail(String email) { Integer fetchOne = jpaQueryFactory .selectOne() .from(qMember) .where(qMember.email.email.eq(email)) .fetchFirst(); return fetchOne != null; }
Reference
JPA exists 쿼리 성능 개선
Avoid Using COUNT() in SQL When You Could Use EXISTS()
JPQL, QueryDsl, Spring Data의 exists