[Django] N+1 쿼리 문제와 select_related

sudog·2023년 9월 14일
0

Django

목록 보기
11/13

쿼리셋은 정말 편리하게 데이터베이스를 다룰 수 있게 해주지만 비효율적으로 많은 쿼리를 날릴 가능성이 있다. 예를 들어서, 다음과 같은 모델이 있다고 생각해보자.

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를 미리 메서드의 인자로 넣어 주면 게시물을 가져올 때 연관된 모든 유저들을 한 번의 쿼리로 가져오게 된다.

profile
안녕하세요

0개의 댓글