TIL no.81 - Django - To Reduce Query

devzunky·2019년 11월 18일
1

Django

목록 보기
30/30

DB를 자주 호출한다면 즉, Query를 많이 한다면 통신은 느려질 수밖에 없습니다.
그렇기 때문에, 줄일 수 있는 Query는 줄이는게 좋습니다.

그리고 Django에는 record(혹은 instance)간의 관계를 미리 읽어들여서
Query를 줄이는 ORM이 존재합니다.

1. select_related(*fields)

다음과 같은 모델을 예로 들겠습니다.

class City(models.Model):
  	name = models.CharField(max_length=20)
    
class Person(models.Model):
  	city = models.ForeignKey(City, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)

class Pet(models.Model):
  	person = models.ForeignKey(Person, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)
# Hits the database.
zunky = Person.objects.get(id=1)

# Hits the database again to get the related City object.
seoul = zunky.city

위의 경우, Person의 instance를 한번 받아오고(zunky)
그 instance가 참조하고 있는 instance(seoul)를 한번 더 받아옵니다.

Django의 ORM을 이용해 이 Query를 줄일 수 있습니다.

# Hits the database.
zunky = Person.objects.select_related('city').get(id=1)

# Doesn't hit the database, because e.blog has been prepopulated
# in the previous query.
seoul = zunky.city

zunky라는 instace를 불러올 때, select_related()라는 QuerySet API를 이용하면
instance가 참조하고 있는 대상을 caching합니다.

그러므로, zunky.city를 읽을 때,
다시 DB로 갈 필요없이 caching되어 있는 값을 읽어오므로
DB 호출을 줄일 수 있습니다.

select_related()는 QuerySet에 적용할 수 있습니다.

for person in Person.objects.filter(city_id=2).select_related('city'):
    # Without select_related(), this would make a database query for each
    # loop iteration in order to fetch the related city for each person.
    print(person.city.name)

filter()와 select_related() 순서는 중요하지 않습니다.
다음 QuerySet은 같습니다.

Person.objects.filter(city_id=2).select_related('city')
Person.objects.select_related('city').filter(city_id=2)

ForeignKey를 더 파고들어 갈 수 있습니다.

class City(models.Model):
  	name = models.CharField(max_length=20)
    
class Person(models.Model):
  	city = models.ForeignKey(City, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)

class Pet(models.Model):
  	person = models.ForeignKey(Person, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)
# Hits the database with joins to the author and hometown tables.
choon_sik = Pet.objects.select_related('person__city').get(id=1)
zunky = choon_sik.person         # Doesn't hit the database.
seoul = zunky.city       # Doesn't hit the database.

# Without select_related()...
choon_sik = Pet.objects.get(id=1)  # Hits the database.
zunky = choon_sik.person           # Hits the database.
seoul = zunky.city       # Hits the database.

여러 Field를 가져오는 것도 가능합니다.

select_related('foo', 'bar')
select_related('foo').select_related('bar')

그리고 위 두 방법은 같은 결과를 초래합니다.

3. prefetch_related(*lookups)

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

위의 select_related()를 사용할 때
Person의 객체인 zunky가 갖고 있는 city는 seoul하나였습니다.

하나의 Field가 ManyToMany 관계라면 어떨까요?

다음과 같은 모델이 있다고 가정하겠습니다.

class City(models.Model):
  	name = models.CharField(max_length=20)

class Language(models.Model):
     name = models.CharField(max_length=100)
 
class Person(models.Model):
     city = models.ForeignKey(City, on_delete=models.CASCADE)
     name = models.CharField(max_length=20)
     language = models.ManyToManyField(Language, through='JoinPersonLanguage')
 
class JoinPersonLanguage(models.Model):
     person = models.ForeignKey(Person, on_delete=models.CASCADE)
     language = models.ForeignKey(Language, on_delete=models.CASCADE)

위의 모델을 보면 Person의 한 객체가 여러 Language instance를 참조할 수 있습니다.

예를 들면, 다음과 같습니다.
zunky: Python, JavaScript, C

이제 Many-To-Many 관계일 때, 효과적으로 DB를 사용하는 방법에 대해 알아보겠습니다.

zunky = Person.objects.prefetch_related('language').get(id=1)
zunky.language.values() # Doesn't hit the database.
<QuerySet [{'id': 1, 'name': 'Python'}, {'id': 2, 'name': 'C'}, {'id': 4, 'name': 'Javascript'}]>

zunky = Person.objects.get(id=1)
zunky.language.values() # Hits the database.
<QuerySet [{'id': 1, 'name': 'Python'}, {'id': 2, 'name': 'C'}, {'id': 4, 'name': 'Javascript'}]>

Many-to-Many 관계인 경우에는
위와 같이 prefetch_related()를 사용합니다.

4. reverse relationships

여태까지는 정방향 참조를 다루었습니다.
Person table에는 "city_id", "language_id" Coulumn이 있기 때문에
정방향 참조가 가능했습니다.

그런데, City를 참조하는 Person을 알고 싶다면 어떻게 하면 될까요?

다음과 같은 모델이 있다고 가정하겠습니다.

class City(models.Model):
     name = models.CharField(max_length=20)

class Language(models.Model):
     name = models.CharField(max_length=100)
 
class Person(models.Model):
     city = models.ForeignKey(City, on_delete=models.CASCADE)
     name = models.CharField(max_length=20)
     language = models.ManyToManyField(Language, through='JoinPersonLanguage')

class JoinPersonLanguage(models.Model):
     person = models.ForeignKey(Person, on_delete=models.CASCADE)
     language = models.ForeignKey(Language, on_delete=models.CASCADE)

City에는 seoul이라는 row가 있습니다.
그리고 seoul을 참조하고 있는 Person의 객체들이 있다고 한다면
다음과 같이 값들을 호출할 수 있습니다.

>>> seoul = City.objects.get(id=1)
>>> seoul.person_set.values()
<QuerySet [{'id': 1, 'city_id': 1, 'name': 'zunky'}, {'id': 4, 'city_id': 1, 'name': 'byeong-min'}, {'id': 5, 'city_id': 1, 'name': 'sae-geul'}]>

현재는 seoul이라는 City의 객체하나뿐이지만,
만약, 모든 City 객체들에 역참조를 해야하는 상황이라면
person_set이라는 Query를 City의 수마다 DB에 요청하게됩니다.

cities = City.objects.all()
for city in cities:
  print(city.person_set.values())

위와 같이 명령을 실행한다면
City의 객체수 만큼 Query를 DB에 요청하게 되는 것입니다.
City의 객체가 seoul, incheon, busan이라면 Query는 3번 요청하게 됩니다.

이런 상황일 때, prefetch_related()를 사용하면 Query를 줄일 수 있습니다.

cities = City.objects.prefetch_related('person_set').all()
for city in cities:
  print(city.person_set.values())

5. Summary

  1. 정방향 참조
  • foreign key, one-to-one과 같이 single-valued relationships의 경우, select_related()를 이용해 Query를 줄인다.
  • Many-To-Many의 경우, prefetch_related()를 이용해 Query를 줄인다.
  1. 역방향 참조
  • 자신을 참조하고 있는 객체를 알고 싶은 경우(Column이 없는 경우),
    prefetch_related()의 lookup에 _set을 이용한 뒤, 원하는 Query를 실행한다.
profile
devzunky@gmail.com

0개의 댓글