반응형
Django ORM에서 select_related와 prefetch_related는 모두 쿼리 최적화를 위한 도구지만, 서로 다른 방식으로 작동한다. 둘의 내부원리 및 동작방식을 비교하고, 언제 각각을 사용해야 하는지 알아본다.
목차
공통점
- 쿼리 최적화 도구: 두 메서드 모두 Django ORM의 쿼리 최적화 도구로, 데이터베이스 쿼리 횟수의 감소가 목적이다.
- N+1 문제 해결: 두 메서드 모두 반복문에서 각 객체마다 추가 쿼리가 발생하는 N+1 쿼리 문제를 해결한다.
- 즉시 로딩(Eager loading): 두 메서드 모두 지연 로딩(lazy loading) 대신 즉시 로딩 방식을 사용하여 필요한 데이터를 미리 가져온다.
- 체이닝 가능: 두 메서드 모두 다른 쿼리셋 메서드들과 체이닝이 가능하다.
Post.objects.select_related('author').filter(is_published=True)
User.objects.prefetch_related('posts').order_by('username')
- 성능 향상: 두 메서드 모두 관련 객체 접근시 추가 데이터베이스 쿼리를 방지하여 애플리케이션 성능을 향상시킨다.
- 메모리 캐싱: 두 메서드 모두 관련 객체 데이터를 메모리에 캐싱하여 재사용 가능하게 한다.
- 함께 사용 가능: 두 메서드는 함께 사용할 수 있으며, 복잡한 모델 관계에서는 종종 두 메서드를 모두 사용한다.
Post.objects.select_related('author').prefetch_related('comments', 'tags')
내부 원리와 동작방식의 차이
select_related
- SQL JOIN 사용: 단일 SQL 쿼리 내에서 외래 키 관계를 JOIN으로 처리한다.
- 정방향 참조(Forward relation)에 최적화: 주로 ForeignKey, OneToOneField 관계에 사용된다.
- 즉시 로딩(Eager loading): 메인 쿼리 실행 시 관련 객체도 함께 가져온다.
prefetch_related
- 별도의 쿼리 실행: 먼저 주 쿼리를 실행한 후, 관련 객체들을 별도의 쿼리로 가져온다.
- 역방향 참조에 적합: ManyToManyField나 역방향 ForeignKey 관계(related_name으로 접근하는 관계)에 적합하다.
- 파이썬 메모리에서 JOIN: 데이터베이스가 아닌 파이썬에서 두 쿼리 결과를 결합한다.
모델 코드
먼저 아래와 같은 모델 코드를 먼저 가정한다.
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
주요 모델 관계:
- OneToOne 관계:
- User와 Profile 사이 (한 유저는 하나의 프로필만 가짐)
- ForeignKey(일대다) 관계:
- Post와 User 사이 (한 유저는 여러 포스트 작성 가능)
- Post와 Category 사이 (한 카테고리는 여러 포스트 포함 가능)
- Comment와 Post 사이 (한 포스트는 여러 댓글 가질 수 있음)
- Comment와 User 사이 (한 유저는 여러 댓글 작성 가능)
- ManyToMany(다대다) 관계:
- Post와 Tag 사이 (한 포스트는 여러 태그 가질 수 있고, 한 태그는 여러 포스트에 사용 가능)
select_related는 언제 사용?
- 단일 객체에 대한 외래 키 관계를 조회할 때 (ForeignKey, OneToOne)
- 관련 객체가 항상 필요하고 관계의 깊이가 깊지 않을 때
예시
# 단일 외래 키 관계
# 각 게시물의 작성자 정보를 한 번에 가져옴
posts = Post.objects.select_related('author').all()
# 중첩된 관계도 가능
posts = Post.objects.select_related('author__profile').all()
# 여러 관계 동시에 가져오기
posts = Post.objects.select_related('author', 'category').all()
select_related는 JOIN을 사용하므로 더 큰 결과 셋을 반환할 수 있지만, 쿼리는 1번만 실행된다.
- SQL JOIN의 동작 방식: JOIN은 두 테이블의 관련 행을 결합하므로, 관계에 따라 결과 행 수가 증가할 수 있다. 특히 일대다 관계에서는 "다" 측 항목만큼 행이 증가한다.
- 중복 데이터 발생: 예를 들어 Post.objects.select_related('author')에서:
- 한 저자가 여러 게시물을 작성했다면, 저자 정보가 각 게시물마다 중복되어 반환됨
- 결과적으로 데이터 크기가 커지지만, 이는 단일 SQL 쿼리로 모든 정보를 가져오기 위해 발생하는 tradeoff임
- N+1 문제 해결: 별도의 쿼리 없이 관련 객체에 접근할 수 있어, 반복문에서 각 객체마다 새 쿼리를 실행하는 N+1 문제를 해결한다.
- 즉시 로딩(Eager loading): 필요한 모든 데이터를 한 번에 가져와 캐시하므로 데이터베이스 왕복 시간을 줄이는 장점이 있다.
결국 select_related는 데이터베이스 호출 횟수를 최소화하는 대신, 더 많은 데이터를 한번에 가져오는 tradeoff가 있다.
prefetch_related는 언제 사용?
- 역방향 관계나 다대다 관계를 조회할 때
- 특히 Post.objects.all()의 각 항목에 대해 여러 관련 객체가 필요할 때
예시
# 역방향 관계 (한 사용자의 모든 게시물)
users = User.objects.prefetch_related('posts').all()
# ManyToMany 관계
posts = Post.objects.prefetch_related('tags').all()
# 복잡한 중첩 prefetch
users = User.objects.prefetch_related(
'posts',
'posts__comments',
'posts__tags'
).all()
prefetch_related는 각 관계마다 별도 쿼리를 실행하지만, 최종 JOIN은 메모리에서 이루어져 복잡한 관계에 더 효율적일 수 있다.
- 데이터베이스 부하 감소: 복잡한 다대다 관계나 역방향 관계를 SQL JOIN으로 처리하면 카테시안 곱(Cartesian product)이 발생하여 결과 행 수가 기하급수적으로 증가할 수 있다. 이는 데이터베이스 서버에 큰 부담을 준다.
- 중복 데이터 최소화: 별도 쿼리를 실행하면 각 객체를 한 번만 가져와 파이썬에서 관계를 구성하므로, 네트워크를 통해 전송되는 중복 데이터가 감소한다.
- 메모리 효율성: 예를 들어, 한 사용자가 1,000개의 게시물을 가진 경우:
- select_related로 JOIN 시: 1,000개 행이 반환되고 사용자 데이터가 1,000번 중복됨
- prefetch_related 사용 시: 사용자 1개 + 게시물 1,000개를 별도로 가져와 파이썬에서 연결
- 쿼리 최적화: 각 테이블에 최적화된 쿼리를 별도로 실행할 수 있어 인덱스 활용이 더 효율적이다.
복잡한 관계에서 select_related 사용시 SQL JOIN은 성능 저하의 원인이 될 수 있으므로, Django는 prefetch_related로 이를 파이썬 메모리에서 처리하는 방식으로 해결한다.
두 메서드를 적절히 활용하면 N+1 쿼리 문제를 해결하고 Django 애플리케이션의 성능을 크게 향상시킬 수 있다.
반응형
'Django' 카테고리의 다른 글
[Django] 모델 필드의 null=True와 blank=True 조건 (0) | 2025.04.05 |
---|---|
[Django] Django Debug Toolbar의 윈도우 환경 MIME type 오동작 해결 (0) | 2025.02.20 |
[Django] CreateView (0) | 2025.02.04 |
[Django] PostgreSQL 연동하기 (1) | 2025.02.04 |
[Django] CBV의 동작 원리와 주요 메서드 분석 (2) | 2025.01.26 |