django 공식문서 표현 대로, 위 2가지는 performance booster다.
related objects를 미리 불러와서(caching) DB에 추가로 접근해야 하는 수고를 덜어준다.
사실 지금이야 프로젝트 규모가 작고 데이터 row가 10~20개정도 뿐이라,
큰 차이가 없다고 느낄 수 있지만 데이터가 몇 만 개 쌓이면 쿼리를 그만큼 많이 발생시켜야 한다. ORM의 대표적인 오류가 바로 이러한 N+1 problem 이다.
What is N+1 problem?
- 쿼리 1번으로 데이터 N건을 갖고왔는데(1 primary query), 참조 정보를 얻기 위해서 N건만큼 2차로 쿼리를 발생시키는 문제를 말한다.
- 발생 원인: django ORM은 lazy loading을 채택하고 있다.
- ORM을 명령할 때 데이터를 갖고 오는 것이 아니라, 해당 명령문이 끝나고 데이터를 불러들여야할 시점이 왔을 때 비로소 쿼리 실행
- 그래서 ORM은 어떤 데이터를 찾아야 하는지 모르는 상태며, 쿼리를 여러번 보내 일일이 확인해야 함
- 해결 방안: eager loading 옵션 수행 (select_related, prefetch_related 등)
#Cart DB를 방문하고, product DB의 name 속성을 갖고 오기 위해 다시 한번 방문
#쿼리 2번
a = Cart.objects.filter(user=request.user)
b = a.product.name
#select_related 사용하면 product DB까지 캐싱되어 한 번에 부를 수 있다
#쿼리 1번
a = Product.objects.select_related('product').get(user=request.user)
b = a.product.name
select_related
와 prefetch_related
는 차이점이 있는데, select_related
가 INNER JOIN 을 하는 원리이기 때문에 single value relationship 에서만 사용할 수 있다. 반면 prefetch_related
는 many-to-many, many-to-one 에도 모두 사용할 수 있다.
또 한 가지 차이는, prefetch_related
는 어쨌든 primary query가 실행된 후 별도의 query를 발생시킨다는 점이다. select_related
는 한번에 query로 related objects를 모두 갖고 온다.
그래서 prefetch_related
만 사용하는 것보다, 2개를 적절히 사용하면 쿼리 수를 가장 많이 줄일 수 있다.
#Cart 인스턴스 쿼리 > product 인스턴스 쿼리 > product들의 image를 갖고 오기 위한 쿼리
#쿼리 3번
Cart.objects.prefetch_related('product__image_set')
#Cart 인스턴스 & product 인스턴스 쿼리 > product들의 image를 갖고 오기 위한 쿼리
#쿼리 2번
Cart.objects.select_related('product').prefetch_related('product__image_set')
실제로 1차 프로젝트에 사용한 코드(장바구니 get)은 아래와 같다.
import django.http import Jsonresponse
def get(self, request):
carts = Cart.objects.filter(user=request.user).select_related('product').prefetch_related('product__image_set')
result = [{
'id' = carts.id,
'image' = [image.image_url for image in cart.product.picture_set.all()]
'name' = carts.product.name,
'price' = carts.product.price,
'quantity' = carts.product.quantity
} for cart in carts]
total_price = Cart.objects.filter(user=request.user).aggregate(sum('price'))
return Jsonresponse({'cart_list':result, 'total_price':total_price(price__sum)}, status=200)
이 블로그에 가면, N+1 문제가 요리의 예시로 재밌게 설명되어 있다.