QuerySet 객체는 평가되기(Evaluated) 전 까지 데이터베이스에 직접 영향을 주지 않는다. QuerySet에 필터링을 하여 또 다른 QuerySet을 얻을 수 있지만, QuerySet이 평가되기 전까지 절대 데이터베이스 Hit를 하는 일이 없다. QuerySet이 평가될 때, SQL 쿼리를 데이터베이스에서 실행하게 된다.
다음과 같은 경우에 QuerySet은 평가된다.
User.objects.all()[:10:2]
people = Preson.objects.filter(age=20)
위와 같이 filter를 걸면 조건에 맞는 로우를 검색 할 수 있는데 이 쿼리셋에 filter를 추가하거나 함수에 전달해도 이는 DB에 아무런 메시지도 전달하지 않는다.
for person in people:
print(person.name)
for person in people:
print(person.job)
쿼리셋을 순회하는 시점에, 쿼리셋에 해당하는 DB의 레코드들을 실제로 가져오며(fetch), 이는 모두 Django 모델로 변환된다. 이를 가리켜 evaluation이라고 한다.
평가된 모델들은 쿼리셋의 내장 캐시에 저장되며, 덕분에 위와 같이 쿼리셋을 다시 순회하더라도 똑같은 쿼리를 DB에 다시 전달하지 않는다.
아래의 내용은 PyCon Korea 2020에서 김성렬님이 강의한 Django ORM(QuerySet) 구조와 원리 그리고 최적화 전략 영상을 참고했다.
# User를 선언하는 시점에는 SQL이 호출되지 않는다.
users = User.objects.all()
# 이 코드에서 실제로 SQL이 호출된다.
list(users)
# 이 코드에서는 SQL이 한 번만 호출된다.
# Order QuerySet과 Company QuerySet을 선언했지만 사용하지 않아서 SQL이 호출되지 않는다.
users = User.objects.all()
orders = Order.objects.all()
companies = Company.objects.all()
list(users)
정말 필요한 만큼만 호출하려는 ORM의 특성 때문에 비효율적으로 ORM이 동작하기도 한다.
users = User.objects.all()
# 0번째 User를 얻고 싶어서 user QuerySet은 SQL을 호출한다.
first_user = users[0]
# 바로 윗줄에서 user 1명 밖에 가져오지 않아서 모든 user를 얻으려면 어쩔 수 없이 다시 SQL을 호출해야 한다.
user_list = list(users)
쿼리셋을 호출하는 순서가 바뀌는 것만으로도 QuerySet 캐싱 때문에 발생하는 SQL이 달라질 수 있다.
users = User.objects.all()
user_list = list(users)
# 위에서 user 쿼리셋이 모든 user를 가져오는 SQL을 이미 호출함. 따라서 0번째 user 쿼리셋에 캐싱된 값을 재활용함
first_user = users[0]
users = User.objects.all()
# 개발자 관점에서는 각 user의 모든 userinfo가 필요한 것을 알지만 QuerySet은 모른다.
for user in users:
# QuerySet입장에서 user의 userinfo가 필요한 시점은 여기다.
# 따라서 userinfo를 알기 위해 SQL을 for문이 돌 때마다 (N번) 호출한다.
user.userinfo
모든 user를 조회하기 위해 SQL 1번, user의 userinfo를 매번 조회하기 위해 SQL을 N번 호출한다.
이러한 N+1 Problem을 해결하기 위해(즉시로딩을 하기 위해) Django에서는 select_related()와 prefetch_related()라는 메서드를 제공한다.
참고사이트
https://superminy.tistory.com/10
https://medium.com/deliverytechkorea/django-queryset-1-14b0cc715eb7
https://docs.djangoproject.com/en/3.1/ref/models/querysets/
https://velog.io/@ikswary/n1-query-problem