쿼리셋은 정말 편리하게 데이터베이스를 다룰 수 있게 해주지만 비효율적으로 많은 쿼리를 날릴 가능성이 있다. 예를 들어서, 다음과 같은 모델이 있다고 생각해보자.
class BookModel(models.Model):
author = models.ForeignKey(UserModel)
...
class UserModel(models.Model):
username = models.charField()
...
한 명의 작가가 많은 책을 집필할 수 있으므로 책과 작가는 다대일 관계이다. 따라서 책이 작가를 FK로 갖게 된다. 만약 아래와 같은 쿼리셋을 실행한다고 생각해 보자.
books = BookModel.objects.all()
for book in books:
print(book.author.username)
책의 저자 이름을 모든 책에 대해 출력하는 과정이다. 그런데, 저자의 username속성을 알기 위해서는 각 책들에 대해서 매번 유저모델 테이블에 저자가 누군지 쿼리를 날려야 한다. 따라서, 만약 책이 N권이라고 한다면, 우리가 기대하는 쿼리는 1번이지만 실제로는 N+1번 실행되게 된다.
실제 데이터와 같이 확인하기 위해 장고의 db.connection을 사용한 예제를 보자.
from django.db import connection
...
a = len(connection.queries)
print(f"실행된 쿼리 수: {a}")
feeds = (CodeModel.objects
.filter(author__track=user.track)
.annotate(Count("likes"))
.order_by("-created_at")
)
for feed in feeds:
print(feed.author.username)
a = len(connection.queries)
print(f"실행된 쿼리 수: {a}")
실행된 쿼리 수: 2
실행 후..
실행된 쿼리 수: 866
1번의 쿼리를 기대했지만 실제로는 모든 게시물에서 1번의 쿼리가 추가로 이뤄지기 때문에 비효율적이다. 이것을 N+1문제라고 부르는데, 이것을 해결하기 위한 방법 역시 장고가 제공하고 있다.
이 문제는 미리 관계된 유저를 모두 가져와 캐싱하는 것으로 해결할 수 있다. select_related
메서드를 사용해 보자.
from django.db import connection
...
feeds = (CodeModel.objects
.filter(author__track=user.track)
.select_related("author")
.annotate(Count("likes"))
.order_by("-created_at")
)
for feed in feeds:
print(feed.author.username)
a = len(connection.queries)
print(f"실행된 쿼리 수: {a}")
실행된 쿼리 수: 2
실행 후..
실행된 쿼리 수: 3
이렇게 FK를 미리 메서드의 인자로 넣어 주면 게시물을 가져올 때 연관된 모든 유저들을 한 번의 쿼리로 가져오게 된다.