[django] select_related, prefetch_related

EMMA·2022년 4월 9일
1
post-custom-banner

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_relatedprefetch_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 문제가 요리의 예시로 재밌게 설명되어 있다.

profile
예비 개발자의 기술 블로그 | explore, explore and explore
post-custom-banner

0개의 댓글