장고는 특정 시기에만 쿼리를 날린다.
그 시기를 알아야 최적화가 가능하다.
ORM에서는 정말 필요한 시점에만 SQL을 호출하는 특징이 있다.
다시 말해, 정말 필요한 시점이 아니면
QuerySet은 SQL을 호출하지 않는다는 것이다
user = User.objects.all() a = user.filter(first_name='a') # 아직 DB에서 데이터를 가져오지않음 order_a = a.order_by('id') # 아직 DB에서 데이터를 가져오지않음 b = user.get(id=1) parent = b.parent.name # << 여기서 쿼리를 날림
users = User.objects.all()
first_user = users[0]
user_list = list(users)
정답은 두 번이다.
첫번째 유저와 모든 유저를 가져오기 위해 User.objects.all()이라는 명령어로 다 가져온듯 싶었지만,
아까 언급했듯이 이 문장은 SQL문을 호출하지 않았다.
아직 데이터를 가져오지 않았다는 것이다.
따라서 첫번째 유저를 가져오기 위해 users[0]라고 했기 때문에, 여기서
SQL문 호출 한 번,
그리고 모든 유저의 정보를 가져오기 위한 list(users).
즉 SQL문이 한 번더 호출되어 총 두 번의 쿼리가 발생했다
for문을 돌 때, 조회할 때마다 sql이 계속 호출되는 문제를 N + 1 문제라고 한다.
(유저를 호출하는 sql 한번) + (유저 개수 N) = (N + 1 개의 쿼리)
이 문제를 해결할 수 있는 아름다운 방법을 소개하고자 한다.
1. select_related
class Child(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Parent(models.Model):
children = models.ForeignKey("Child", related_name="parent", on_delete=models.CASCADE)
name = models.CharField(max_length=100)
def __str__(self):
return self.name
위 모델은 1:N으로 이루어진 관계형 모델이다.
qs = Parent.objects.select_related("children")
위 쿼리를 통해 최소한의 쿼리로(중복 쿼리를 줄이고) 데이터를 가져올 수 있음을 기대한다.
성능 향상에 도움이 되는 것이다.
위 사진은 select_related 없이 데이터를 가져오는 예시를 보여주는 사진인데
유사하거나 중복되는 쿼리가 계속 반복됨을 알 수 있다.
하지만 select_related를 사용하면 최소한의 쿼리만으로 데이터를 불러오는 것을 알 수 있다.
2. prefetch_related
qs = children.objects.prefetch_related("parent")
역방향 참조로 Child 입장에서 사용한 모습이다.
prefet_related를 사용하면 select_related와 마찬가지로
중복되는 쿼리를 줄일 수 있으며 성능향상을 기대해 볼 수 있다.
3. Prefetch
Prefetch(lookup, queryset=None, to_attr=None)
Prefetch의 원형이다.
우리는 추가적인 쿼리셋을 제어하고 싶을 때가 많다.
그럴 때 prefetch_related 안에서 Prefetch를 사용하면 된다.
from django.db.models import Prefetch
queryset = post_models.Parent.objects.prefetch_related(
Prefetch(
"child",
queryset=post_models.Child.objects.prefetch_related(
Prefetch(
"child_of_child",
queryset=post_models.ChildofChild.objects.prefetch_related("
"child_of_child_of_child"
")
)
)
))
위 형태로 데이터 테이블끼리 깊게 연결된 형태도 제어할 수 있다.