select_related와 prefetch_related

select_relatedprefetch_related 는 하나의 QuerySet을 가져올 때, 미리 related objects들까지 다 불러와주는 함수이다. 비록 query를 복잡하게 만들긴 하지만, 그렇게 불러온 data들은 모두 cache에 남아있게 되므로 DB에 다시 접근해야 하는 수고를 덜어줄 수 있다.
이렇게 두 함수 모두 DB에 접근하는 수를 줄여, performance를 향상시켜준다는 측면에서는 공통점이 있지만, 그 방식에는 차이점이 있다.


먼저 select_related 은 SQL의 JOIN을 사용하는 특성상 foreign-key(또는 many-to-one) , one-to-one 와 같은 single-valued relationships에서만 사용이 가능하다는 한계가 있다.

아래와 같은 모델이 있다고 가정해보자.

from django.db import models


class Country(models.Model):

    name = models.CharField(
        max_length=10,
    )

    def __str__(self):
        return self.name


class Person(models.Model):

    city = models.ForeignKey(City)

    name = models.CharField(
        max_length=10,
    )

    def __str__(self):
        return self.name


class Pet(models.Model):

    person = models.ForeignKey(Person)

    name = models.CharField(
        max_length=10,
    )

    def __str__(self):
        return self.name

여기서 Pet 모델에서의 id가 1번인 instance의 personcountry를 뽑아오기 위해 아래와 같이 두 가지 방법을 사용하였다.

# Not use select_related()

pet = Pet.objects.get(id=1)  
person = pet.person  
country = person.country  
# Use select_related()

pet = Pet.objects.select_related('person__country').get(id=1)  
person = pet.person  
country = person.country  

출력결과로는 두 가지 경우의 차이점을 볼 수가 없다. 하지만 DB에 접근하는 관점으로 보면 큰 차이가 난다.

첫번째 방법으로 하면 총 DB에 세 번 접근하게 된다.

  1. Pet 모델에서 id가 1번인 pet을 가져오기 위한 query
  2. 그 pet의 person를 가져오기 위한 query
  3. 그 person의 country를 가져오기 위한 query

하지만 두번째 방법으로 하면 Pet.objects.select_related('person__country').get(id=1)에서 이미 related objects(여기에서는 person, country)까지 다 뽑아와 cache에 저장해놓게 된다.
따라서 그 다음에는 person, country를 가져오기 위해 다시 DB에 접근하지 않고 cache에서 꺼내 쓰면 된다.

반면, prefetch_relatedforeign-key , one-to-one 뿐만 아니라 many-to-many , one-to-many 등 모든 relationships에서 사용 가능하다.

다음과 같이 Person 모델과 N:M 관계를 가지는 Language라는 모델을 하나 추가하였다.

class Language(models.Model):

    person_set = models.ManyToManyField(Person)

    name = models.CharField(
        max_length=10,
    )

    def __str__(self):
        return self.name

여기서 Person의 모든 instance들과 그 instance들의 language_set들을 아래와 같이 모두 출력해야 한다고 가정하자.

Tom: Python Ruby
Peter: Python Node.js Java
John: Java C++ php

아까와 마찬가지로 두 가지 방법을 사용하였다.

# No use prefetch_related()

people = Person.objects.all()  
for person in people:  
    print(person.name+" : ", end="")
    for language in person.language_set.all():
        print(language.name+" ", end="")
    print("")
# Use prefetch_related()

people = Person.objects.all().prefetch_related('language_set')  
for person in people:  
    print(person.name+" : ", end="")
    for language in person.language_set.all():
        print(language.name+" ", end="")
    print("")

첫번째 경우에서는 Person.objects.all() 안에 있는 person마다 person.language_set.all() 이라는 query가 실행이 된다. 즉, person이 100개 있다면 person.language_set.all() 이라는 query가 DB로 100번 날라간다는 의미이다.

하지만 두번째 경우에서처럼 prefetch_related 를 쓰게 된다면, 이 부분을 엄청나게 효율적으로 개선할 수 있다. Person.objects.all()라는 query가 동일하게 실행됨과 동시에 self.language_set.all() 이라는 query가 별도로 실행돼 받아온 data들이 cache에 저장되게 된다. 그래서 person의 수 만큼 person.language_set.all()이 실행되더라도 DB에 접근하지 않고 cache에서 찾아서 쓰게 된다.
따라서 결과적으로 2개의 query만으로 아까와 똑같은 결과를 내게 된다.

여기까지만 보면 모든 relationships에서 사용할 수 있는 prefetch_related 가 더 좋아보이고, prefetch_related 로 할 수 있는 것을 굳이 select_related 를 사용해야되나 라는 아직 의문이 든다.
하지만 이 외에 두 함수가 동작하는 방식에도 중요한 차이점이 있다. 위에서 잠깐 설명하긴 했지만, prefetch_related 은 원래의 main query가 실행된 후 별도의 query를 따로 실행하게 된다.
반면, select_related 은 하나의 query만으로 related objects들을 다 가져온다.

Pet.objects.prefetch_related('person') # 2 queries  
Pet.objects.select_related('person') # 1 query  

즉, 완벽히 동일한 결과라도 prefetch_related 를 쓰느냐, select_related 를 쓰느냐에 따라 query의 수가 달라진다.

아래의 예는 prefetch_relatedselect_related 를 어떻게 적절히 사용해야 하는지를 잘 보여준다.

Pet.objects.prefetch_related('person__language_set')  

위의 코드는 prefetch_related만 사용하였고, 결과적으로 3개의 query가 순차적으로 실행하게 된다.

  1. Pet의 모든 instance를 가져오기 위한 query
  2. 그 Pet instace들의 person을 가져오기 위한 query
  3. 그 person들의 language_set을 가져오기 위한 query

여기에 select_related 를 적절히 사용하면 query 수를 더 줄일 수 있다.

Pet.objects.select_related('person').prefetch_related('person__language_set')  

select_related 에서 이미 person에 대한 data까지 모두 가져왔으므로, prefetch_related 에서 person은 cache를 통해 가져오고, laguage_set만 DB에서 fetch해오면 된다.
따라서 총 2개의 query만 실행된다.

  1. Pet의 모든 instance와 그 intance의 person을 가져오는 query
  2. 그 person들의 language_set을 가져오기 위한 query

글을 마치며

many-to-many , one-to-many 과 같은 relationships에서는 어쩔 수 없이 prefetch_related 를 사용하여야겠지만, foreign-key(또는 many-to-one) , one-to-one 와 같은 single-valued relationships이 있는 곳에서는 최대한 select_related 를 사용하여 query 수를 줄여주는 것이 효과적일 것 같다.
또 cache가 되는 과정을 잘 판단하여 어떻게 하면 DB에 접근하는 것을 최소로 할 수 있을까에 대해 항상 끈임없이 고민하여야 할 것 같다.

참고