본문 바로가기

프로젝트/집안일 관리 시스템

[성능 개선] 데이터베이스로 날라가는 쿼리의 수를 줄여보자

버전

ngrinder 3.5.8

mysql 8.0.35

 

 

쿼리 줄이기

N+1 문제

JPA를 사용하면서 Fetch 전략을 기본적으로 LAZY 전략을 사용했는데 이때 N+1 문제가 발생할 수 있다.

N+1 문제란 1번의 쿼리를 날렸을 때 의도치 않은 N번의 쿼리가 추가적으로 발생하는 문제로

LAZY 전략을 사용해서 엔티티를 가져온 후 일대다 연관관계를 가진 하위 엔티티를 조회할 경우 발생할 수 있다.

 

해결방법으로 fetch join을 사용할 수 있다.

하지만 페이징을 사용할 경우 데이터베이스로 날아가는 쿼리에는 limit과 offset이 없는데

이는 select 한 데이터들을 메모리에 저장하고 애플리케이션 레벨에서 정렬하기 때문인데 부하가 많이 걸린다.

 

다른 해결방법으로는 batch size를 사용할 수 있다.

엔티티를 가져온 후 연관관계를 가진 하위 엔티티를 조회할 때 in query를 사용해서

설정한 크기만큼 한 번에 가져올 수 있다.

Batch Insert

insert 문이 여러 개 날아갈 경우 이를 한 번의 insert 문으로 처리할 수 있다.

 

MySQL client에서 server로 보낼 수 있는 패킷 크기는 제한되어 있기 때문에 이에 유의해야 한다.

 

 

반복할일

N+1 문제 해결하기

반복할일 엔티티를 페이징해서 조회한 후

일대다 연관관계를 가진 반복일 엔티티를 조회하는데 이때 N+1 문제가 발생한다.

1개의 쿼리를 날려서 10개의 반복할일이 조회될 경우

반복일을 조회하는 10개의 쿼리가 추가적으로 날아간다.

select
    repeattodo0_.todo_id as todo_id2_4_,
    repeattodo0_.created_time as created_3_4_,
    repeattodo0_.modified_time as modified4_4_,
    repeattodo0_.deadline as deadline5_4_,
    repeattodo0_.delete_status as delete_s6_4_,
    repeattodo0_.room_id as room_id7_4_,
    repeattodo0_.todo_item as todo_ite8_4_,
    repeattodo0_.users_id as users_id9_4_ 
from
    todo repeattodo0_ 
where
    repeattodo0_.dtype='R' 
    and repeattodo0_.room_id=? 
    and repeattodo0_.delete_status=? 
order by
    repeattodo0_.created_time desc limit ?
select
    repeatdayl0_.todo_id as todo_id3_2_0_,
    repeatdayl0_.repeat_day_id as repeat_d1_2_0_,
    repeatdayl0_.repeat_day_id as repeat_d1_2_1_,
    repeatdayl0_.day as day2_2_1_,
    repeatdayl0_.todo_id as todo_id3_2_1_ 
from
    repeat_day repeatdayl0_ 
where
    repeatdayl0_.todo_id=?

 

batch size를 적용하면 in query를 사용해서 한 번에 가져온다.

select
    repeatdayl0_.todo_id as todo_id3_2_1_,
    repeatdayl0_.repeat_day_id as repeat_d1_2_1_,
    repeatdayl0_.repeat_day_id as repeat_d1_2_0_,
    repeatdayl0_.day as day2_2_0_,
    repeatdayl0_.todo_id as todo_id3_2_0_ 
from
    repeat_day repeatdayl0_ 
where
    repeatdayl0_.todo_id in (
        ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
    )

Batch Insert 사용하기

기존에는 cascade 설정을 통해 반복할일 엔티티를 영속화하고 저장하면서

자식 엔티티인 반복일 엔티티도 같이 영속화하고 저장되도록 했다.

이때 insert 문은 여러 개 날아간다.

INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)
INSERT INTO repeat_day (day, todo_id) VALUES (?, ?)

 

다음과 같이 한 번의 insert 문으로 저장되도록 한다.

INSERT INTO repeat_day (day, todo_id) VALUES (?, ?),(?, ?),(?, ?),(?, ?),(?, ?),(?, ?),(?, ?)

테스트하기

300명의 가상유저를 설정하고 시간은 5분으로 설정했다. 

스레드를 초기화하는 부분에서 유저가 로그인을 하도록 하고

반복할일 생성과 반복할일 조회하는 부분을 테스트했다.

결과적으로 성능 지표라고 볼 수 있는 TPS가 1.18배 향상된 것을 확인할 수 있다.

적용 전
적용 후

 

 

투표

N+1 문제 해결하기

투표 엔티티와 투표항목 엔티티를 조회한 후

투표항목마다 투표한 유저들의 정보를 조회하는데 이때 N+1 문제가 발생한다.

1개의 쿼리를 날려서 1개의 투표가 조회되고

1개의 쿼리를 날려서 3개의 투표항목이 조회될 경우

투표한 유저들의 정보를 조회하는 3개의 쿼리가 추가적으로 날아간다.

select
    vote0_.vote_id as vote_id1_8_,
    vote0_.created_time as created_2_8_,
    vote0_.modified_time as modified3_8_,
    vote0_.delete_status as delete_s4_8_,
    vote0_.vote_status as vote_sta5_8_,
    vote0_.room_id as room_id6_8_,
    vote0_.title as title7_8_,
    vote0_.users_id as users_id8_8_ 
from
    vote vote0_ 
where
    vote0_.vote_id=? 
    and vote0_.delete_status=?

select
    voteitem0_.vote_item_id as vote_ite1_9_,
    voteitem0_.created_time as created_2_9_,
    voteitem0_.modified_time as modified3_9_,
    voteitem0_.choice as choice4_9_,
    voteitem0_.vote_id as vote_id5_9_ 
from
    vote_item voteitem0_ 
where
    voteitem0_.vote_id=?
select
    user1_.profile_img as col_0_0_,
    user1_.nickname as col_1_0_,
    user1_.users_id as col_2_0_ 
from
    user_vote_item uservoteit0_ 
inner join
    users user1_ 
        on (
            uservoteit0_.users_id=user1_.users_id
        ) 
where
    uservoteit0_.vote_item_id=? 
    and uservoteit0_.delete_status=?

 

in query를 써서 한 번에 가져오도록 쿼리를 수정한다.

select
    user1_.profile_img as col_0_0_,
    user1_.nickname as col_1_0_,
    user1_.users_id as col_2_0_,
    voteitem2_.choice as col_3_0_,
    uservoteit0_.vote_item_id as col_4_0_ 
from
    user_vote_item uservoteit0_ 
inner join
    users user1_ 
        on (
            uservoteit0_.users_id=user1_.users_id
        ) cross 
join
    vote_item voteitem2_ 
where
    uservoteit0_.vote_item_id=voteitem2_.vote_item_id 
    and uservoteit0_.delete_status=? 
    and (
        uservoteit0_.vote_item_id in (
            ? , ? , ?
        )
    )

Batch Insert 사용하기

기존에는 cascade 설정을 통해 투표 엔티티를 영속화하고 저장하면서

자식 엔티티인 투표 항목 엔티티도 같이 영속화하고 저장되도록 했다.

이때도 insert 문은 여러 개 날아간다.

INSERT INTO vote_item (choice, vote_id) VALUES (?, ?)
INSERT INTO vote_item (choice, vote_id) VALUES (?, ?)
INSERT INTO vote_item (choice, vote_id) VALUES (?, ?)

 

다음과 같이 한 번의 insert 문으로 저장되도록 한다.

INSERT INTO vote_item (choice, vote_id) VALUES (?, ?),(?, ?),(?, ?)

테스트하기

300명의 가상유저를 설정하고 시간은 5분으로 설정했다. 

스레드를 초기화하는 부분에서 유저가 로그인을 하도록 하고

투표 생성과 투표 조회하는 부분을 테스트했다.

TPS가 1.1배 향상된 것을 확인할 수 있다.

적용 전
적용 후