FrameWork/Django

N + 1 쿼리 문제

JHeaon 2024. 7. 10. 11:50

 

오늘은 n + 1 문제에 대해서 알아보고자 한다. 

 


 

N + 1 문제

연관관계에서 발생하는 문제로, 연관 관계가 설정된 엔티티를 조회할 경우에 데이터 개수(N)만큼 연관관계의 조회쿼리가 추가로 발생하여 데이터를 읽어오게 되는 문제를 말한다. 

 

 

예를 들어 아래처럼 각 모델과 연관관계를 설정해 두었다고 가정한다. 

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=20)
    contents = models.TextField()


class Comment(models.Model):
    contents = models.TextField()
    post = models.ForeignKey("Post", on_delete=models.CASCADE)

 

 

 

해당 상황에서 댓글에 대한 포스팅글에 정참조를 하고자 하였을 때 쿼리는 다음과 같다. 

  • 참고 : 장고 앱 이름이 mall이라서 데이터베이스이름에 앱_데이터베이스이름으로 나타나고 있다.
>>> comments = Comment.objects.all()
>>> for comment in comments:
...     comment.post

(0.001) SELECT "mall_comment"."id", "mall_comment"."contents", "mall_comment"."post_id" FROM "mall_comment"; args=(); alias=default

(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 2 LIMIT 21; args=(2,); alias=default
<Post: Post object (2)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 3 LIMIT 21; args=(3,); alias=default
<Post: Post object (3)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>
(0.000) SELECT "mall_post"."id", "mall_post"."title", "mall_post"."contents" FROM "mall_post" WHERE "mall_post"."id" = 1 LIMIT 21; args=(1,); alias=default
<Post: Post object (1)>

 

 

이런 일이 일어나는 이유는 Django ORM이 Lazy-loading을 기본값으로 사용하고 있기 때문에 자체적으로 이를 최적화하여 최소한의 쿼리만 날리기 때문이다. 이를 하나씩 풀어서 설명하자면 다음과 같다. 

 

1. comment = Comment.objects.all()에서는 장고의 지연로딩 때문에 쿼리가 발생하지 않는다. 

 

2. for comment in comments을 통해  'SELECT "mall_comment"."id", "mall_comment"."contents", "mall_comment"."post_id" FROM "mall_comment"; args=(); alias=default'이라는 comment에 대한 전체 조회 쿼리를 발생시키게 되고, comment에 대한 정보들이 캐시에 저장된다.

 

3. 이제는 순회하면서 comment에 대한 post의 정보를 조회하는데 이때, 캐시에는 comment에 대한 정보만 있고 post에 대한 정보는 없다. 따라서 다시 조회 쿼리를 발생시키게 된다. 조회 쿼리는 데이터 개수(N) 만큼 일어난다. 

 

 

 

 

 

 

해결 방법

연관관계에 있는 데이터를 조회하기 전에 연관관계에 있는 데이터를 모두 가져온 뒤, 미리 캐시에 저장시켜 캐시에서 데이터를 꺼내는 방식을 통해 해결할 수 있다. 이때 사용하는 방법이 select_related와 prefetch_related이다. 

 

 

select_related와 prefetch_related

오늘은 select_related와 prefetch_related에 대해서 정리해보고자 한다. 해당 주제를 다루기 전에 Django의 ORM 특징과 쿼리 셋에 대해서 알고 가면 좀 더 이해에 도움이 된다. https://jheaon.tistory.com/274 Django

jheaon.tistory.com

 

이를 사용하여 다시 코드를 작성하면 아래와 같이 확연하게 조회쿼리를 줄일 수 있다. 

>>> comments = Comment.objects.select_related("post")
>>> for comment in comments:
...     comment.post
... 

"""
SELECT "mall_comment"."id", "mall_comment"."contents", "mall_comment"."post_id", "mall_post"."id", "mall_post"."title", "mall_post"."contents" 
FROM "mall_comment" I
NNER JOIN "mall_post" ON ("mall_comment"."post_id" = "mall_post"."id"); args=(); alias=default

<Post: Post object (1)>
<Post: Post object (1)>
<Post: Post object (2)>
<Post: Post object (3)>
<Post: Post object (1)>
<Post: Post object (1)>
<Post: Post object (1)>
<Post: Post object (1)>
<Post: Post object (1)>
<Post: Post object (1)>
"""

'FrameWork > Django' 카테고리의 다른 글

유저 커스텀 모델 사용하기  (0) 2024.07.29
Django에서 static, media 관리하기  (0) 2024.07.29
select_related와 prefetch_related  (0) 2024.07.10
orm과 queryset  (0) 2024.07.10
django-seed을 통해 더미데이터 만들기  (0) 2023.12.13

'FrameWork/Django'의 다른글

  • 현재글 N + 1 쿼리 문제

관련글