Post

Django ORM

Django ORM

N+1 문제

  • 하나의 쿼리로 N개의 데이터를 가져온 후, 각 데이터에 연결된 데이터를 얻기 위해 N번의 추가 쿼리가 발생하는 현상

  • example

    • 100개의 게시글 목록을 가져오고, 각 게시글의 작성자 이름을 표시해야 하는 경우
      1
      2
      3
      
      posts = Post.objects.all()
      for post in posts:
      	print(post.author.name)
      
    • 위 코드는 총 101번 (1 + 100)의 쿼리를 실행하게 되어 심각한 성능 저하를 발생시킴
  • 정방향 관계를 위한 SQL JOIN
    • Foreign Key, OneToOneField와 같은 정방향 관계에서 N+1 문제를 해결하기 위해 사용
1
2
3
4
5
posts = Post.objects.select_related("author").all()
# 단 한 번의 쿼리로 게시글과 작성자 정보를 함께 가져옴

for post in posts:
	print(post.author.name)
  • select_related("author") 을 통해 Post 모델과 author 필드로 연결된 User 모델을 JOIN 하여, 게시글 정보를 가져올 때 작성자 정보도 함께 가져옴!
    • → 루프 안에서 post.author.name 에 접근할 때 추가 쿼리가 발생하지 않음
  • 역방향 및 다대다 관계를 위한 별도 쿼리 후 조합
    • ManyToManyField, ForeignKey의 역참조 관계에서 N+1 문제 해결에 효과적

    • select_related와 달리 각 관계에 대해 별도의 쿼리를 실행한 후 python 레벨에서 합침

1
2
3
4
5
6
7
posts = Post.objects.prefetch_related("comment_set").all()
# 2번의 쿼리로 게시글과 모든 댓글 정보를 가져옴

for post in posts:
	print(f"--- {post.title}의 댓글 ---")
	for comment in post.comment_set.all():  # 추가 쿼리가 발생하지 않음
		print(comment.content)
  • prefetch_related('comment_set') 는 먼저 모든 게시글을 가져오는 쿼리를 실행하고, 그 다음 가져온 게시글들의 ID를 사용하여 해당 게시글에 달린 모든 댓글을 가져오는 두번째 쿼리 실행
    • 이후 django가 파이썬에서 게시글과 댓글 연결 → 루프 안에서 post.comment_set.all() 을 호출해도 추가적인 DB 접근이 발생하지 않음

aggregate

  • 쿼리셋 전체에 대한 집계 값을 계산하여 dictionary 형태로 반환
1
2
3
4
5
from django.db.models import Count, Avg

# 전체 게시글 수와 평균 조회수 계산
summary = Post.objects.aggregate(total_posts=Count("id"), avg_views=Avg("views"))
print(summary)   # {'total_posts': 100, 'avg_views': 150.5}

annotate

  • 쿼리셋의 각 객체에 대해 별도의 필드를 추가하여 반환
1
2
3
4
5
6
from django.db.models import Count

# 각 게시글별 댓글 수를 계산하여 'comment_count' 필드로 추가
posts = Post.objects.annotate(comment_count=Count("comment"))
for post in posts:
	print(f"{post.title} (댓글: {post.comment_count})")
  • 각 Post 객체에 comment_count라는 새로운 속성을 추가하여, 추가적인 쿼리 없이 각 게시글의 댓글 수를 알 수 있게 해줌

Subquery

  • 쿼리 안에 또 다른 쿼리를 포함시키는 기능

  • 다른 쿼리의 결과를 현재 쿼리의 필터링이나 annotation에 활용할 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 각 게시글마다 가장 최근에 달린 댓글의 내용을 함께 표시

from django.db.models import OuterRef, Subquery

# 각 게시글(OuterRef('pk'))의 댓글 중 가장 최신 댓글의 content를 가져오는 서브쿼리
latest_comment_subquery = Comment.objects.fillter(
	post=OuterRef("pk")
).order_by("-created_at").values("content")[:1]

# annotate로 각 post에 latest_comment 필드 추가
posts = Post.objects.annotate(
	latest_comment=Subquery(latest_comment_subquery)
)

for post in posts:
	print(f"{post.title} (최신 댓글: {post.latest_comment})")
  • Subquery를 사용하면 루프를 돌며 각 객체에 대해 추가적인 쿼리를 실행하는 대신, 데이터베이스 레벨에서 효율적으로 원하는 정보를 계산하고 가져올 수 있음

OuterRef

  • Subquery를 사용할 때, 서브 쿼리 내부에서 외부 쿼리의 필드를 참조할 수 있게 해주는 역할

  • 일반적으로 서브 쿼리는 독립적으로 실행되기 때문에, 자신이 포함된 외부 쿼리의 정보에 접근할 수 없음

  • example
    • 각 게시글에 대해 해당 게시글에 달린 댓글 중 가장 최신 댓글 내용을 가져오는 경우

    • 이 작업을 하려면 외부 쿼리 (게시글 목록)의 각 행을 처리할 때마다, 해당 행의 게시글 ID를 통해 서브 쿼리 (댓글 검색)의 조건을 동적으로 바꿔야 함

      • 이때 OuterRef가 해당 행의 게시글 ID를 서브 쿼리에 전달!
  • Subquery와 함께 annotate()나 filter() 내부에서 주로 사용됨

  • 파이썬 코드에서 루프를 돌며 여러 번 쿼리를 실행하는 대신, 단일 쿼리 내에서 데이터베이스가 효율적으로 행 별 계산을 수행하도록 하여 성능 최적화
This post is licensed under CC BY 4.0 by the author.