Prefetch 객체와 Subquery로 복잡한 N+1 문제 해결하기

Django ORM에서 기본 prefetch_related로 해결되지 않는 복잡한 N+1 문제를 Prefetch 객체와 Subquery를 활용해 효율적으로 해결하는 방법

기본 N+1 문제와 한계

# ❌ N+1 문제 발생 예시
categories = Category.objects.all()
for category in categories:
    public_posts = category.posts.filter(is_public=True)  # 각 루프마다 쿼리 발생

# ✅ prefetch_related 사용
categories = Category.objects.prefetch_related('posts')  # 2개 쿼리로 해결

단순 prefetch_related는 모든 관련 객체를 가져오므로 필터링된 데이터가 필요할 때 한계 존재

Prefetch 객체: 세밀한 제어

prefetch_related의 동작을 커스터마이징하여 필터링, 정렬, 슬라이싱 등을 DB 레벨에서 처리

from django.db.models import Prefetch

# 공개된 포스트만 미리 가져오기
public_posts_qs = Post.objects.filter(is_public=True)
categories = Category.objects.prefetch_related(
    Prefetch('posts', queryset=public_posts_qs, to_attr='public_posts')
)

# 최근 댓글 3개만 가져오기
latest_comments_qs = Comment.objects.order_by('-created_at')
posts = Post.objects.prefetch_related(
    Prefetch('comments', queryset=latest_comments_qs, to_attr='latest_comments')
)

to_attr 사용으로 기존 관계와 충돌 없이 필터링된 결과를 새로운 속성에 저장

Subquery와 OuterRef: 단일 값 추출

관련 객체의 특정 값 하나를 메인 쿼리에 붙여서 가져올 때 사용

from django.db.models import Subquery, OuterRef

# 각 포스트의 가장 최근 댓글 내용 가져오기
latest_comment_subquery = Comment.objects.filter(
    post=OuterRef('pk')
).order_by('-created_at').values('content')[:1]

posts = Post.objects.annotate(
    latest_comment_content=Subquery(latest_comment_subquery)
)

# 각 카테고리의 공개 포스트 개수 세기
public_post_count_subquery = Post.objects.filter(
    category=OuterRef('pk'),
    is_public=True
).values('category').annotate(
    count=Count('pk')
).values('count')

categories = Category.objects.annotate(
    public_post_count=Subquery(public_post_count_subquery, output_field=IntegerField())
)

OuterRef로 바깥쪽 쿼리의 필드를 참조하여 상관 서브쿼리 작성

Prefetch vs Subquery 선택 기준

구분
Prefetch
Subquery

목적

관련 객체 목록 가져오기

관련 객체의 단일 값 가져오기

결과

모델 인스턴스 리스트

새로운 필드(속성)

쿼리 수

2개

1개

사용 시기

객체들을 루프로 처리할 때

개수, 집계값, 존재 여부 확인할 때

실무 적용 전략

  • 기본적으로 select_related와 prefetch_related 먼저 적용

  • 필터링/정렬이 필요하면 Prefetch 객체 사용

  • 단일 값(개수, 최신값 등)이 필요하면 Subquery 사용

  • django-debug-toolbar로 실행되는 SQL 쿼리 확인 필수

  • DRF Serializer에서 SerializerMethodField 대신 annotate 활용

Last updated