select_related와 prefetch_related를 사용하는 이유는 ORM에서 발생되는 N+1 문제를 해결하기 위해 eager-loading(즉시 호출) 방식을 취하는 두 가지 메서드 사용한다.
N+1 문제란?
Lazy-loading 방식은 실제 데이터를 사용할 때 DB에 히트하게 된다. 이로 인해 참조 관계의 데이터를 가져올 때 문제가 발생한다. 처음 1번의 SQL query를 실행할 때 해당 모델이 갖고 있는 필드만을 가져온다. 순회 시 참조 모델의 데이터는 처음 쿼리에서 가져오지 않았기 때문에 순회 횟수(N번)만큼 참조 관계 모델의 SQL query를 실행한다. 꼭 1번 더 쿼리를 실행하는 것은 아니고 비효율적으로 쿼리 호출 횟수가 생긴다는 의미로 받아들이면 된다.
자세한 내용은 1. 블로그 2.블로그 참조
Eager-loading 이란?
Lazy-loading과 반대되는 개념으로 queyset이 evaluate 될 때 쿼리를 실행하는 게 아닌, queryset 인스턴스를 생성할 때 Join을 걸어 관계되어 있는 모델의 데이터까지 SQL query를 사전에 실행하는 방법이다.
1. select_related()
사용하는 경우는 다음과 같다.
- foreign-key, one-to-one처럼 single-valued 관계에서만 사용이 가능하다. SQL의 JOIN을 사용하는 방법이다.
- 1:N 관계의 N(FK의 정참조)이 사용할 수 있다.
- 외부 키 관계를 "따라서" 실행할 때 관련 개체 데이터를 추가로 선택하는 Querysets를 반환한다.
# Hits the database.
e = Entry.objects.get(id=5)
# Hits the database again to get the related Blog object.
b = e.blog
- 아래와 같이 select_related를 실행한다면? DB에 다시 hit 하지 않는다.
# Hits the database.
e = Entry.objects.select_related('blog').get(id=5)
# Doesn't hit the database, because e.blog has been prepopulated
# in the previous query.
b = e.blog
- select_related()를 개체의 쿼리 집합과 함께 사용할 수 있다.
from django.utils import timezone
# Find all the blogs with entries scheduled to be published in the future.
blogs = set()
for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related('blog'):
# Without select_related(), this would make a database query for each
# loop iteration in order to fetch the related blog for each entry.
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())
- 외부 키를 쿼리 하는 것과 유사한 방식으로 외부 키를 따를 수 있다. 다음과 같은 모델이 있는 경우
from django.db import models
class City(models.Model):
# ...
pass
class Person(models.Model):
# ...
hometown = models.ForeignKey(
City,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
class Book(models.Model):
# ...
author = models.ForeignKey(Person, on_delete=models.CASCADE)
Book.objects.select_related('author__hometown'). get(id=4)의 queryset 호출은 관련 사용자 및 관련 도시를 캐싱한다.
# 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.
2. prefetch_related()
사용하는 경우는 다음과 같다.
- many-to-many
- many-to-one
지정한 각 조회에 대해 관련 개체를 일괄적으로 자동으로 검색하는 QuerySet을 반환한다. select_related와 비슷한 목적을 가지고 있다. 둘 다 관련 객체에 액세스 하여 발생하는 데이터베이스 쿼리의 홍수를 막기 위해 설계되었지, 전략은 상당히 다르다.
select_related는 SQL JOIN을 생성하고 SELECT 문에 관련 개체의 필드를 포함하여 동작한다. select_related는 동일한 데이터베이스 쿼리에서 관련 개체를 가져온다. 그러나 '많은(many-to-many)' 관계에 걸쳐 결합함으로써 발생하는 훨씬 더 큰 결과 집합을 피하기 위해 select_related는 단일 값 관계(one-to-one, one-to-many:FK)로 제한한다.
반면에 prefetch_related는 각 관계에 대해 별도의 조회를 수행하고 Python에서 'Joining'을 실행한다. 이를 통해 select_related에서 수행할 수 없는 many-to-many와 many-to-one 개체를 미리 가져올 수 있다. 또한 추가 쿼리가 발생한다.
예를 들면,
from django.db import models
class Topping(models.Model):
name = models.CharField(max_length=30)
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
def __str__(self):
return "%s (%s)" % (
self.name,
", ".join(topping.name for topping in self.toppings.all()),
)
실행하면,
>>> Pizza.objects.all()
["Hawaiian (ham, pineapple)", "Seafood (prawns, smoked salmon)"...
이것의 문제는 Pizza.__str__()이 self.topings.all()을 요청할 때 데이터베이스를 쿼리해야 하므로 Pizza.objects.all()은 Pizza QuerySet의 모든 항목에 대해 Toping 테이블에서 쿼리를 실행한다.
prefetch_related를 사용하여 쿼리를 두 개로 줄일 수 있다.
>>> Pizza.objects.prefetch_related('toppings')
이것은 각 피자에 대한 self.tops.all()을 의미하며, 이제 매번 self.tops.all()이 호출될 때마다 항목을 위해 데이터베이스로 이동하는 대신 단일 쿼리에 채워져 있는 사전 추출된 쿼리 세트 캐시에서 항목을 찾을 수 있다. 즉, 모든 관련 토핑은 단일 쿼리로 가져와 관련 결과의 캐시가 미리 채워진 queryset를 만드는 데 사용된다. 이러한 queryset는 self.tops.all() 호출에 사용된다.
prefetch_related()의 추가 쿼리는 QuerySet이 평가되기 시작하고 기본 쿼리가 실행된 후에 실행된.
일반 조인 구문을 사용하여 관련 필드의 관련 필드를 수행할 수도 있습니다. 위의 예에 추가 모델이 있다고 가정하자.
class Restaurant(models.Model):
pizzas = models.ManyToManyField(Pizza, related_name='restaurants')
best_pizza = models.ForeignKey(Pizza, related_name='championed_by', on_delete=models.CASCADE)
다음의 경우는 모두 사용 가능하다.
>>> Restaurant.objects.prefetch_related('pizzas__toppings')
이렇게 하면 레스토랑에 속한 모든 피자와 해당 피자에 속한 모든 토핑을 미리 가져온다. 그러면 총 3개의 데이터베이스 쿼리가 생성된다. 하나는 레스토랑, 하나는 피자, 다른 하나는 토핑이다.
>>> Restaurant.objects.prefetch_related('best_pizza__toppings')
이건 최고의 피자와 각 레스토랑의 최고의 피자에 대한 모든 토핑을 가져올 것이다. 이것은 3개의 데이터베이스 쿼리에서 수행된다. 하나는 레스토랑용, 하나는 최고의 피자, 다른 하나는 토핑이다.
쿼리 수를 2개로 줄이기 위해 select_related를 사용하여 best_pizza의 관계를 가져올 수도 있다.
>>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')
prefetch는 기본 쿼리(select_related에서 필요한 조인 포함) 이후에 실행되므로 best_pizza 객체가 이미 fetch 되었음을 감지할 수 있으며, 다시 fetch 하지 않는다.
또한 Prefetch 개체를 사용하여 prefetch 작업을 추가로 제어할 수 있다. 가장 간단한 형태로 Prefetch는 전통적인 문자열 기반 조회와 동일하다.
>>> from django.db.models import Prefetch
>>> Restaurant.objects.prefetch_related(Prefetch('pizzas__toppings'))
queryset인수를 사용하여 사용자 지정 queryset를 제공할 수 있다. 이 명령을 사용하여 queryset의 기본 순서를 변경할 수 있다.
>>> Restaurant.objects.prefetch_related(
... Prefetch('pizzas__toppings', queryset=Toppings.objects.order_by('name')))
또는 select_related()를 호출하여 쿼리 수를 더욱 줄일 수 있다.
>>> Pizza.objects.prefetch_related(
... Prefetch('restaurants', queryset=Restaurant.objects.select_related('best_pizza')))
필터링된 결과를 관련 관리자의 캐시에 저장하는 것보다 덜 모호하므로 사전 추출 결과를 필터링할 때 to_attr을 사용하는 것이 좋다.
>>> queryset = Pizza.objects.filter(vegetarian=True)
>>>
>>> # Recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
... Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
>>> vegetarian_pizzas = restaurants[0].vegetarian_pizzas
>>>
>>> # Not recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
... Prefetch('pizzas', queryset=queryset))
>>> vegetarian_pizzas = restaurants[0].pizzas.all()
Prefetch에 대한 자세한 정보는 이곳을 참고하자.
prefetch_related에 관한 정리는 다음에 다시 해봐야겠다. 이번 포스팅으로 명확하게는 이해가 되지않았다.
참고
1. https://docs.djangoproject.com/en/4.1/ref/models/querysets/#select-related
2. https://scoutapm.com/blog/django-and-the-n1-queries-problem
4. https://cocook.tistory.com/52
6.https://docs.djangoproject.com/en/4.1/ref/models/querysets/#django.db.models.Prefetch
'Django' 카테고리의 다른 글
[QuerySet] Q() Objects 알아보기 (0) | 2023.01.18 |
---|---|
[QuerySet] Built-in Expressions - F() 표현식 알아보기 (0) | 2023.01.17 |
[DRF] HyperlinkedModelSerializer 살펴보기 (0) | 2023.01.12 |
[DRF] ModelSerializer 살펴보기 (0) | 2023.01.11 |
Django의 특징 (1) | 2022.12.24 |