django select_related, prefetch_related, Prefetch 정리

개발자 강세영·2022년 6월 3일
0

TIL

목록 보기
37/65

select_related, prefetch_related는 모두 장고에서 기본으로 제공하는 기능으로 ORM 최적화를 위한 것이다. 두 가지 모두 DB에 접근(hit)하는 횟수를 줄이고 더 빠르게 데이터를 조회할 수 있게 해준다.
이 두개를 쓰면 안쓰는것보다 SQL 쿼리문 자체는 약간 복잡해지지만 한번 불러온 데이터들을 캐싱하기 때문에 매 쿼리마다 DB에 접근하지 않고 캐싱된 것을 불러오게 되므로 성능이 개선된다.
또한 두 가지 모두 Eager Loading을 하기 때문에 ORM의 N+1 문제까지 해결할 수 있다.

  • 객체가 역참조하는 single object(one-to-one or many-to-one)이거나, 또는 정참조 foreign key 인 겨우
  • 각각의 lookup마다 SQL의 JOIN을 실행하여 테이블의 일부를 가져오고, select .. from에서 관련된 필드들을 가져온다
  • 괄호안에 따옴표로 필드명이나 클래스명을 소문자로 입력하면 된다. related_name 옵션이 적용된다.
  • filter()와 select_related()의 순서는 상관없다고 한다.
  • select_related('foo', 'bar') 같이 여러 값을 한꺼번에 넣을 수 있다.
  • select_related()같이 괄호안에 빈값을 넣으면 null값이 없는 모든 foreignkey 필드를 참조한다고 한다.
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.
  • 객체가 정참조 multiple objects(many-to-many or one-to-many)이거나, 또는 역참조 Foreign Key인 경우
  • 각 관계 별로 DB 쿼리를 수행하고, 파이썬 단에서 JOIN을 수행한다.
  • 괄호안에 따옴표로 필드명이나 클래스명을 소문자로 입력하면 된다. related_name 옵션이 적용된다.
  • prefetch_related()를 쓰는 경우 반복문 안에서는 .first() 보다 .all()[0]을 활용하는게 좋다.

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() # 역참조

Prefetch

  • Prefetch 객체는 prefetch_related()을 더 확실하게 제어하기 위해 사용된다.
  • select_related와 prefetch_related는 views파일에서 장고 models 객체를 한개라도 임포트했다면 따로 임포트하지 않아도 사용할 수 있지만 Prefetch 객체는 Q, Sum, Count, F등과 같이 임포트 해줘야 한다. 임포트 문은 다음과 같다.

from django.db.models import Prefectch

  • Prefetch의 to_attr 속성을 잘 활용하면 좋다.
만약 첫 번째 레스토랑에서 채식주의자가 먹을 수 있는 피자를 조회하는 쿼리는 아래와 같습니다.

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

0개의 댓글