N + 1문제
문제 상황
- 문제가 발생하는 상품 목록 조회 API는 아래와 같은 상품들을 12개 씩 조회하는 API입니다.
- 문제점은 JPA 지연로딩으로 인해 상품에 걸려있는 입찰과 좋아요의 쿼리가 상품의 개수만큼 날아갑니다.
즉 상품이 12개라면 1( 상품들을 조회하기 위한 쿼리 ) + 12(상품 별 입찰 목록) + 12(상품 별 좋아요 목록)
총 25개의 쿼리가 날아가게 됩니다. - 좋아요와 입찰 모두 1 대 N 관계이기 때문에 fetch join을 쓴다면 페이징 처리로 인해 OOM문제가 발생합니다.
해결책 1(Batch Size - in Query)
- Batch Size를 조정하여 조회한 상품 별로 입찰과 좋아요를 조회하는 것이 아닌 12개의 상품에 대한 좋아요와 입찰을 한 번에 가져옵니다.
- 즉, 1(상품 12개 조회) + 1(상품 12개에 해당하는 좋아요 목록 조회) + 1(상품 12개에 해당하는 입찰 목록 조회) 총 3개의 쿼리가 발생합니다.
실제 쿼리
select *
from item
order by created_at DESC
limit 12 offset 1;
select *
from
bid
where
bid.item_id in(?,?,?,?,?,?,?,?,?,?,?,?)
select *
from
likeable_item
where
likeable_item.item_id in(?,?,?,?,?,?,?,?,?,?,?,?)
해결책 2(Sub Query)
- select 절에 스칼라 서브 쿼리를 날려 한방 쿼리로 작성이 가능합니다.
- 성능 테스트를 진행해 봤을 때 서브쿼리가 조금 더 성능이 좋은것을 확인했습니다.
요약하자면, 대부분의 경우에는 서브쿼리가 성능이 좋고 특히, 상품 하나에 걸려있는 입찰과 좋아요의 수가 증가할수록 성능이 향상되는 것을 확인할 수 있었습니다.
-> 예상으로는 In쿼리의 경우 상품들의 입찰과 좋아요를 모두 퍼올리고 애플리케이션 단에서 처리해야 하기 때문에 성능이 저하된 것 같습니다. - 테스트 케이스가 많지 않아서 검증되지 못한 해결책이라 판단하여 추후 테스트 보강 후 리팩토링 할 예정입니다.
실제 쿼리
select *,
(select max(bid.price) from bid where bid.item_id = item.id),
(select count(1) from bid where bid.item_id = item.id),
(select count(1) from likeable_item where likeable_item.item_id = item.id),
(1 in (select member_id from likeable_item where likeable_item.item_id = item_id))
from item
order by created_at DESC
limit 12 offset 1;
Using filesort
- 상품 목록 조회 API는 정렬 조건으로 만료일자, 조회수, 생성일지, 낙찰가 등이 이용됩니다.
- order by에 해당 컬럼이 이용될 때 인덱스 없이 정렬을 진행하며 성능 저하를 발생시킵니다.
- 정렬에 필요한 컬럼에 단일 인덱스를 걸어 해결했습니다.