Django ORM 쿼리 최적화

차민철·2022년 4월 27일
0

장고는 특정 시기에만 쿼리를 날린다.
그 시기를 알아야 최적화가 가능하다.

Django ORM의 특징

  • Lazy Loading: 지연 호출
  • Eager Loading: 즉시 로딩(N+1 Problem)

N+1 문제

for문을 돌때, 조회할 때마다 sql이 계속 호출되는 문제를 N+1문제라고 한다.
(유저를 호출하는 sql 한번) + (유저 개수 N) = (N+1 개의 쿼리)

이 문제를 해결할 수 있는 아름다운 방법을 소개하고자 한다.

select_related()

select_related(*fields)

  • ForeignKey, OneToOneField에서 사용가능
  • OneToOneFiled의 reverse direction으로 related_name을 사용해서 접근 가능하다.
  • 어떠한 것을 불러올지 모를 경우 select_related()의 괄호안에 아무것도 넣지 않으면 전부 다 불러오게 되지만 지양하는 것이 좋다.(불필요한 내용들을 포함하도록 쿼리를 요청할 수 있기 때문)
  • select_related로 연결된 값들을 초기화 하고 싶으면 None 을 괄호안에 넣어주면 된다.
  • select_related('foo', 'bar')과 select_related('foo').select_related('bar')는 동일하다.
# database에 쿼리를 날린다.
e = Entry.objects.get(id=5)
# Entry에 연관된 blog의 정보를 불러오기 위해 데이터베이스에 한번 더 요청
b = e.blog

위의 쿼리는 database에 2번 요청 위의 쿼리를 select_related로 최적화했을 때 1번의 쿼리요청만으로 원하는 내용을 불러올 수 있다.

# database에 쿼리를 날린다.
e = Entry.objects.select_related('blog').get(id=5)
# database에 쿼리를 날리지않는다. (이미 첫번째 쿼리에서 e.blog를 불러왔기 때문)
b = e.blog
  1. select_related를 사용하지 않으면 e.blog를 부를때마다 query를 새로 요청하게 될 것이다.
from django.utils import timezone

blogs = set()

for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related('blog'):
    blogs.add(e.blog)

filter와 select_related의 순서는 중요하지 않다. 다음 두 쿼리는 동일하다.

Entry.objects.filter(pub_date__gt=timezone.now()).select_related('blog')
Entry.objects.select_related('blog').filter(pub_date__gt=timezone.now())
  1. select_related는 2개 모델의 연관을 한번에 받아올 수 있다.
# Hits the database with joins to the author and hometown tables.
b = Book.objects.select_related('author__hometown').get(id=4)
p = b.author         # Doesn't hit the database.
c = p.hometown       # Doesn't hit the database.

# Without select_related()...
b = Book.objects.get(id=4)  # Hits the database.
p = b.author         # Hits the database.
c = p.hometown       # Hits the database.

첫번째 select_related를 사용한 경우는 b.author.hometown을 전부 다 받아온다.
두번째 select_related를 사용하지 않은 경우 author, hometown을 각각 불러올 때마다 database에 새로운 쿼리를 요청한다.

prefetch_related

prefetch_related(*lookups)

  • prefetch_related는 select_related와 같이 data를 Cache에 저장하며, 모든 relationships에서 사용이 가능하다.
  • ManyToManyFileds 또는 reverse ForeignKey에서 사용한다.

select_related vs prefetch_related

  • select_related는 하나의 query로 related object들을 불러오지만, prefetch_related는 main query가 실행이 된 후 별도의 query가 실행이 된다. 따라서 1번 이상 쿼리가 진행되기 때문에 가급적 select_related를 사용하는 것이 리소스 소모를 줄일 수 있다.
Pizza.objects.all().prefetch_related('toppings')

가능한 쿼리목록

Restaurant.objects.prefetch_related('pizzas__toppings')
# 레스토랑, 피자, 토핑에 대해서 3개의 쿼리 생성
Restaurant.objects.prefetch_related('best_pizza__toppings')
# 쿼리수를 2개로 줄이기 위해 select_related도 사용
Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')

Prefetch명령어를 사용해서 추가적인 prefetch operation을 조정할 수 있다.
다음과 같이 사용할 수 있으며 order_by와 같은 작업을 통해 순서를 바꿔주는 등의 작업을 할 수 있다.
또한 Prefetch안에 select_related도 넣어줄 수 있다.

Restaurant.objects.prefetch_related(Prefetch('pizzas__toppings'))
Restaurant.objects.prefetch_related(
     Prefetch('pizzas__toppings', queryset=Toppings.objects.order_by('name')))
Pizza.objects.prefetch_related(
     Prefetch('restaurants', queryset=Restaurant.objects.select_related('best_pizza')))

prefetch_related를 사용하면 select_related와 마찬가지로 중복되는 쿼리를 줄일 수 있으며 성능향상을 기대해 볼 수 있다.

Prefetch

  • 추가 쿼리셋을 제어하는 방법
  • prefetch_related()를 제어하는데 사용된다.
Prefetch(lookup, queryset=None, to_attr=None)

Prefetch의 원형이다.
우리는 추가적인 쿼리셋을 제어하고 싶을 때가 많다.
그럴 때 prefetch_related안에서 Prefetch를 사용하면 된다.

subquery

class Subquery(queryset, output_field=None)

Post.objects.annotate(newest_commenter_email=Subquery(newest.values('email')[:1]))
Comment.objects.filter(post__in=Subquery(posts.values('pk')))
comments = Comment.objects.filter(post=OuterRef('pk')).order_by().values('post')
total_comments = comments.annotate(total=Sum('length')).values('total')
Post.objects.filter(length__gt=Subquery(total_comments))

aggregate과 annotate

aggretate를 통해 값들을 만들어낸 후 annotate을 통해 이름을 부여해준다.

예시들

Company.objects.annotate(num_products=Count('products'))
Company.objects.annotate(num_products=Count(F('products')))
Company.objects.annotate(num_offerings=Count(F('products') + F('services')))

0개의 댓글