select_related와 prefetch_related는 무엇인가
select_related 와 prefetch_related 는 하나의 QuerySet을 가져올 때, 미리 related objects들까지 다 불러와주는 함수이다. 비록 query를 복잡하게 만들긴 하지만, 그렇게 불러온 data들은 모두 cache에 남아있게 되므로 DB에 다시 접근해야 하는 수고를 덜어줄 수 있다.
이렇게 두 함수 모두 DB에 접근하는 수를 줄여, performance를 향상시켜준다는 측면에서는 공통점이 있지만, 그 방식에는 차이점이 있다.
select_related와 prefetch_related 비교
먼저 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의 person
과 country
를 뽑아오기 위해 아래와 같이 두 가지 방법을 사용하였다.
# 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에 세 번 접근하게 된다.
- Pet 모델에서 id가 1번인 pet을 가져오기 위한 query
- 그 pet의 person를 가져오기 위한 query
- 그 person의 country를 가져오기 위한 query
하지만 두번째 방법으로 하면 Pet.objects.select_related('person__country').get(id=1)
에서 이미 related objects(여기에서는 person
, country
)까지 다 뽑아와 cache에 저장해놓게 된다.
따라서 그 다음에는 person
, country
를 가져오기 위해 다시 DB에 접근하지 않고 cache에서 꺼내 쓰면 된다.
반면, prefetch_related 은 foreign-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_related 와 select_related 를 어떻게 적절히 사용해야 하는지를 잘 보여준다.
Pet.objects.prefetch_related('person__language_set')
위의 코드는 prefetch_related만 사용하였고, 결과적으로 3개의 query가 순차적으로 실행하게 된다.
- Pet의 모든 instance를 가져오기 위한 query
- 그 Pet instace들의 person을 가져오기 위한 query
- 그 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만 실행된다.
- Pet의 모든 instance와 그 intance의 person을 가져오는 query
- 그 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에 접근하는 것을 최소로 할 수 있을까에 대해 항상 끈임없이 고민하여야 할 것 같다.