✅ N+1 Select 문제 해결

Yuri Lee·2020년 12월 10일
0

스터디 목록 조회하는 쿼리 발생
스터디 마다 쿼리가 3개씩
태그 조회, 지역조회, 멤버 조회 3n+1

스터디가 9개가 있으면 3*9+1 = 28 개

JPA를 쓰면서 성능이 느려지는 이슈중 하나이다.

left (outer) join + fetchJoin + distinct로 해결

left join 을 해야한다. 왜? fetchJoin 을 하기 위해서!

그럼 fetchJoin 은 왜 할까? study data 를 가져올 때 다른 데이터도 같이 가져오기 위해서 (연관된..)

left (outer) join

  • 첫번째(left) 테이블에 연관 관계가 있는 모든 데이터 가져오기. 연관 데이터가 없으면
    null로 채워서라도...
  • 첫번째 테이블 컬럼만 본다면 중복 row 발생

사실 right, inner join 등 다양한 join 이 있는데 이 중에 사용하면 된다. 지금 우리는 study 기준으로 study와 연관된 데이터를 가져와야 하므로 left join 을 해야 한다.

left join을 했더니 데이터가 2개에서 3개가 되었다. 왜 ?..
지금 study 만 가져와서 그렇지만, 사실 이대로 복사해서 db 실행해서 실제 데이터를 보면 세건이 나온다. 그렇다면 왜 세건일까?

left join 은 왼쪽에 해당하는 테이블의 데이터를 다 가져온다. 오른쪽에 맵핑되는 데이터와 같이 가져온다.

사실 중복이 아니다. title 까지 찍어보면 아니다.
다른 데이터이다.

세번째는 연관되어있는 태그가 1개밖에 없어서 1개가 나온 것,
연관되어 있는 태그가 2개면 달라진다.

태그 정보를 빼고 study 정보만 파싱해서 가져왔기 때문에 스터디가 3개 나오는 게 맞다. 관계형 db는 원래 이렇다.

조인, 데이터를 합친다. 여러가지 방법 중에 left join 을 한다. 왼쪽 테이블에 있는 데이터를 전부다 보여줌. 오른쪽에 연관되어있는 데이터가 없더라도...(이때는 null이 나옴)
하지만 연관된 데이터가 있으면 연관된 수만큼 반복이 되어서 나온다. 그래서 여러줄이 나온 것이다.

우리가 원래 하려던 작업으로 돌아와서 ..

fetchJoin

  • join 관계의 데이터도 같이 가져온다.

join을 했으므로 이제 fetchjoin을 할 수 있다. fetchjoin은 join한 데이터를 가져오라는 것이다. 이제부터 tag 정보를 가져오게 된다.

.leftJoin(study.tags, QTag.tag).fetchJoin()
.leftJoin(study.zones, QZone.zone).fetchJoin()
.leftJoin(study.members, QAccount.account).fetchJoin();

이제 query 는 1개만 가져올 텐데 .. 문제는 중복 데이터 이다.

distinct

  • 중복 제거

결과 중에서 유일한 값만!

    select
        distinct study0_.id as id1_7_0_,
        tag2_.id as id1_12_1_,
        zone4_.id as id1_13_2_,
        account6_.id as id1_0_3_,
        study0_.closed as closed2_7_0_,
        study0_.closed_date_time as closed_d3_7_0_,
        study0_.full_description as full_des4_7_0_,
        study0_.image as image5_7_0_,
        study0_.path as path6_7_0_,
        study0_.published as publishe7_7_0_,
        study0_.published_date_time as publishe8_7_0_,
        study0_.recruiting as recruiti9_7_0_,
        study0_.recruiting_updated_date_time as recruit10_7_0_,
        study0_.short_description as short_d11_7_0_,
        study0_.title as title12_7_0_,
        study0_.use_banner as use_ban13_7_0_,
        tag2_.title as title2_12_1_,
        tags1_.study_id as study_id1_10_0__,
        tags1_.tags_id as tags_id2_10_0__,
        zone4_.city as city2_13_2_,
        zone4_.local_name_of_city as local_na3_13_2_,
        zone4_.province as province4_13_2_,
        zones3_.study_id as study_id1_11_1__,
        zones3_.zones_id as zones_id2_11_1__,
        account6_.bio as bio2_0_3_,
        account6_.email as email3_0_3_,
        account6_.email_check_token as email_ch4_0_3_,
        account6_.email_check_token_generated_at as email_ch5_0_3_,
        account6_.email_verified as email_ve6_0_3_,
        account6_.joined_at as joined_a7_0_3_,
        account6_.location as location8_0_3_,
        account6_.nickname as nickname9_0_3_,
        account6_.occupation as occupat10_0_3_,
        account6_.password as passwor11_0_3_,
        account6_.profile_image as profile12_0_3_,
        account6_.study_created_by_email as study_c13_0_3_,
        account6_.study_created_by_web as study_c14_0_3_,
        account6_.study_enrollment_result_by_email as study_e15_0_3_,
        account6_.study_enrollment_result_by_web as study_e16_0_3_,
        account6_.study_updated_by_email as study_u17_0_3_,
        account6_.study_updated_by_web as study_u18_0_3_,
        account6_.url as url19_0_3_,
        members5_.study_id as study_id1_9_2__,
        members5_.members_id as members_2_9_2__ 
    from
        study study0_ 
    left outer join
        study_tags tags1_ 
            on study0_.id=tags1_.study_id 
    left outer join
        tag tag2_ 
            on tags1_.tags_id=tag2_.id 
    left outer join
        study_zones zones3_ 
            on study0_.id=zones3_.study_id 
    left outer join
        zone zone4_ 
            on zones3_.zones_id=zone4_.id 
    left outer join
        study_members members5_ 
            on study0_.id=members5_.study_id 
    left outer join
        account account6_ 
            on members5_.members_id=account6_.id 
    where
        study0_.published=? 
        and (
            lower(study0_.title) like ? escape '!'
        ) 
        or exists (
            select
                1 
            from
                study_tags tags7_,
                tag tag8_ 
            where
                study0_.id=tags7_.study_id 
                and tags7_.tags_id=tag8_.id 
                and (
                    lower(tag8_.title) like ? escape '!'
                )
        ) 
        or exists (
            select
                1 
            from
                study_zones zones9_,
                zone zone10_ 
            where
                study0_.id=zones9_.study_id 
                and zones9_.zones_id=zone10_.id 
                and (
                    lower(zone10_.local_name_of_city) like ? escape '!'
                )
        )

distinct 는 의미가 없다. 가장 이상적인 것은 distinct 를 빼도 ..... (?)
3개를 가져온다.

전체 줄에 대한 distinct 이다.

distinct는 크게 의미가 없다. 하지만 이것을 붙임으로써 쿼리 결과를 JPA 가 해석할 때 distinct 라는 리절트만 받아온다. 그래서 결과적으로 fetch 했을 때 두개만 볼 수 있는 것이다. 걸러 냈기 때문에

여기서 쿼리 최적화를 더 할 수 있다.
1. distinct를 빼고 리저트 트랜스폼을 제공
2. 가져오는 데이터가 여전히 너무 많다. 필요로 하는 것은 기껏 해봐야 study id, path, title, short descript, tag의 이름들, 지역의 이름들, summary해온 값들 만 필요함


출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발

profile
Step by step goes a long way ✨

0개의 댓글