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)의 쿼리를 실행하게 되어 심각한 성능 저하를 발생시킴
- 100개의 게시글 목록을 가져오고, 각 게시글의 작성자 이름을 표시해야 하는 경우
selected_related
- 정방향 관계를 위한 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
에 접근할 때 추가 쿼리가 발생하지 않음
- → 루프 안에서
prefetch_related
- 역방향 및 다대다 관계를 위한 별도 쿼리 후 조합
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 접근이 발생하지 않음
- 이후 django가 파이썬에서 게시글과 댓글 연결 → 루프 안에서
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.