select_related, prefetch_related는 모두 장고에서 기본으로 제공하는 기능으로 ORM 최적화를 위한 것이다. 두 가지 모두 DB에 접근(hit)하는 횟수를 줄이고 더 빠르게 데이터를 조회할 수 있게 해준다.
이 두개를 쓰면 안쓰는것보다 SQL 쿼리문 자체는 약간 복잡해지지만 한번 불러온 데이터들을 캐싱하기 때문에 매 쿼리마다 DB에 접근하지 않고 캐싱된 것을 불러오게 되므로 성능이 개선된다.
또한 두 가지 모두 Eager Loading을 하기 때문에 ORM의 N+1 문제까지 해결할 수 있다.
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)
… then a call to Book.objects.select_related('author__hometown').get(id=4) will cache the related Person and the related City:
# 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()와 prefetch_related() 중 어느 것이 사용 가능한지는 모델 객체의 .__dir__()으로 확인가능하다.
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()
# The problem with this is that every time Pizza.__str__() asks for self.toppings.all()
it has to query the database, so Pizza.objects.all() will run a query on the Toppings table
for every item in the Pizza QuerySet.
We can reduce to just two queries using prefetch_related:
Pizza.objects.all().prefetch_related('toppings')
class ExampleA(models.Model):
pass
class ExampleB(models.Model):
a = ForeignKey(ExampleA)
ExampleB.objects.select_related('a').all() # 정참조
ExampleA.objects.prefetch_related('exampleb_set').all() # 역참조
from django.db.models import Prefectch
만약 첫 번째 레스토랑에서 채식주의자가 먹을 수 있는 피자를 조회하는 쿼리는 아래와 같습니다.
queryset = Pizza.objects.filter(vegetarian=True)
restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset))
vegetarian_pizzas = restaurants[0].pizzas.all()
만약 위와 같은 queryset이 다른 조인 쿼리에도 사용된다면 해당 쿼리가 실행될 때마다 새로 조회를 하므로 중복조회가 발생됩니다.
이때, Prefetch()에서 제공하는 to_attr을 사용하여 쿼리를 메모리에 저장하여 효율적으로 사용할 수 있습니다.
queryset = Pizza.objects.filter(vegetarian=True)
restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
vegetarian_pizzas = restaurants[0].vegetarian_pizzas
to_attr에 저장되는 Prefetch()의 데이터 크기가 너무 크지 않다면,
메모리에 올려 재사용성을 늘리는 것이 효율적입니다.
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()
출처:
https://docs.djangoproject.com/ko/4.0/ref/models/querysets/
https://wave1994.tistory.com/70
https://brownbears.tistory.com/433