Django QuerySet 최적화: values(), values_list(), only(), defer() 완벽 가이드
Django ORM을 사용하다 보면 데이터베이스 성능 최적화가 중요한 이슈가 됩니다. 특히 대용량 데이터를 다룰 때는 필요한 필드만 선택적으로 가져오는 것이 성능에 큰 영향을 미칩니다. 이 글에서는 Django에서 제공하는 4가지 주요...
Django ORM을 사용하다 보면 데이터베이스 성능 최적화가 중요한 이슈가 됩니다. 특히 대용량 데이터를 다룰 때는 필요한 필드만 선택적으로 가져오는 것이 성능에 큰 영향을 미칩니다. 이 글에서는 Django에서 제공하는 4가지 주요 QuerySet 최적화 메서드인 values(), values_list(), only(), defer()의 차이점과 성능 영향을 상세히 알아보겠습니다.
📚 테스트 환경 설정
먼저 예제에서 사용할 모델을 정의해보겠습니다.
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
bio = models.TextField()
birth_date = models.DateField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
summary = models.CharField(max_length=500)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
published_date = models.DateTimeField()
view_count = models.IntegerField(default=0)
is_published = models.BooleanField(default=False)
def __str__(self):
return self.title
🔍 1. values() 메서드
기본 개념
values()는 QuerySet의 결과를 딕셔너리 형태로 반환합니다. 지정된 필드만 데이터베이스에서 가져오므로 메모리 사용량과 네트워크 트래픽을 줄일 수 있습니다.
사용법과 예제
# 기본 사용법
articles = Article.objects.values('title', 'author__name', 'published_date')
print(articles.query) # 생성된 SQL 확인
# 출력 결과
for article in articles:
print(article)
# {'title': 'Django 튜토리얼', 'author__name': '김개발', 'published_date': datetime.datetime(...)}
실제 SQL 분석
-- values()로 생성되는 SQL
SELECT "article"."title", "author"."name", "article"."published_date"
FROM "article"
INNER JOIN "author" ON ("article"."author_id" = "author"."id")
-- 전체 필드를 가져오는 일반 QuerySet
SELECT "article"."id", "article"."title", "article"."content",
"article"."summary", "article"."author_id", "article"."published_date",
"article"."view_count", "article"."is_published"
FROM "article"
활용 시나리오
# 1. API 응답용 데이터 준비
def get_article_list_api(request):
articles = Article.objects.filter(is_published=True).values(
'id', 'title', 'summary', 'author__name', 'published_date'
)
return JsonResponse({'articles': list(articles)})
# 2. 대시보드용 통계 데이터
def get_dashboard_stats():
stats = Article.objects.values('author__name').annotate(
article_count=Count('id'),
total_views=Sum('view_count'),
avg_views=Avg('view_count')
)
return list(stats)
# 3. 검색 결과 최적화
def search_articles(query):
return Article.objects.filter(
title__icontains=query
).values(
'id', 'title', 'summary', 'author__name'
)[:20] # 상위 20개만
📋 2. values_list() 메서드
기본 개념
values_list()는 결과를 튜플 형태로 반환합니다. 딕셔너리보다 메모리 효율이 좋고, 단순한 데이터 처리에 적합합니다.
사용법과 옵션
# 기본 사용법 - 튜플 반환
articles = Article.objects.values_list('title', 'author__name', 'view_count')
for article in articles:
print(article)
# ('Django 튜토리얼', '김개발', 1250)
# flat=True - 단일 필드의 경우 평면 리스트 반환
titles = Article.objects.values_list('title', flat=True)
print(list(titles))
# ['Django 튜토리얼', 'Python 기초', 'REST API 설계']
# named=True - 명명된 튜플 반환 (Django 4.2+)
from collections import namedtuple
articles = Article.objects.values_list('title', 'view_count', named=True)
for article in articles:
print(f"제목: {article.title}, 조회수: {article.view_count}")
성능 비교 예제
import time
from django.db import connection
# 성능 테스트 함수
def measure_query_performance(queryset, description):
start_time = time.time()
list(queryset) # 강제 평가
end_time = time.time()
print(f"{description}")
print(f"실행 시간: {end_time - start_time:.4f}초")
print(f"SQL 쿼리 수: {len(connection.queries)}")
print("=" * 50)
# 테스트 실행
def performance_comparison():
# 전체 객체 조회
full_objects = Article.objects.all()
measure_query_performance(full_objects, "전체 객체 조회")
# values() 사용
values_query = Article.objects.values('title', 'view_count')
measure_query_performance(values_query, "values() 사용")
# values_list() 사용
values_list_query = Article.objects.values_list('title', 'view_count')
measure_query_performance(values_list_query, "values_list() 사용")
실용적 활용 예제
# 1. 선택 옵션 생성
def get_author_choices():
return Article.objects.values_list('author_id', 'author__name').distinct()
# 2. 데이터 내보내기
def export_article_data():
articles = Article.objects.values_list(
'title', 'author__name', 'published_date', 'view_count'
)
import csv
with open('articles.csv', 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['제목', '작성자', '발행일', '조회수'])
writer.writerows(articles)
# 3. 간단한 통계 계산
def get_view_stats():
view_counts = Article.objects.values_list('view_count', flat=True)
return {
'total': sum(view_counts),
'average': sum(view_counts) / len(view_counts),
'max': max(view_counts),
'min': min(view_counts)
}
🎯 3. only() 메서드
기본 개념
only()는 지정된 필드만 데이터베이스에서 가져오지만, 모델 인스턴스를 반환합니다. 다른 필드에 접근하면 추가 쿼리가 발생합니다.
기본 사용법
# only() 사용 - 지정된 필드만 로드
articles = Article.objects.only('title', 'published_date')
for article in articles:
print(article.title) # OK - 이미 로드된 필드
print(article.published_date) # OK - 이미 로드된 필드
print(article.content) # 추가 DB 쿼리 발생!
지연 로딩 동작 분석
# 지연 로딩 테스트
def test_deferred_loading():
# only()로 제한된 필드만 로드
article = Article.objects.only('title').first()
print("=== 초기 로드된 필드 ===")
print(f"제목: {article.title}") # 쿼리 없음
print("=== 추가 필드 접근 ===")
print(f"내용: {article.content}") # 새로운 쿼리 발생
# 필드별 로드 상태 확인
print(f"지연 필드들: {article.get_deferred_fields()}")
ForeignKey와 only() 활용
# 관계 필드 최적화
def optimized_article_view():
# 작성자 정보도 함께 최적화
articles = Article.objects.select_related('author').only(
'title', 'summary', 'published_date',
'author__name', 'author__email'
)
for article in articles:
print(f"제목: {article.title}")
print(f"작성자: {article.author.name}") # 추가 쿼리 없음
print(f"이메일: {article.author.email}") # 추가 쿼리 없음
# print(f"바이오: {article.author.bio}") # 추가 쿼리 발생!
# prefetch_related와 only() 조합
def complex_optimization():
authors = Author.objects.prefetch_related(
Prefetch(
'article_set',
queryset=Article.objects.only('title', 'view_count')
)
).only('name', 'email')
for author in authors:
print(f"작성자: {author.name}")
for article in author.article_set.all():
print(f" - {article.title}: {article.view_count}회")
⏱️ 4. defer() 메서드
기본 개념
defer()는 only()의 반대 개념으로, 지정된 필드를 제외한 모든 필드를 로드합니다. 큰 텍스트 필드나 바이너리 필드를 제외할 때 유용합니다.
기본 사용법
# defer() 사용 - 지정된 필드 제외하고 로드
articles = Article.objects.defer('content', 'summary')
for article in articles:
print(article.title) # OK - 로드된 필드
print(article.view_count) # OK - 로드된 필드
print(article.content) # 추가 DB 쿼리 발생!
대용량 필드 처리
# 대용량 텍스트 필드 최적화
def article_list_view():
# content 필드는 크기가 크므로 목록에서 제외
articles = Article.objects.defer('content').filter(is_published=True)
context = {
'articles': articles,
'total_count': articles.count()
}
return render(request, 'articles/list.html', context)
def article_detail_view(request, article_id):
# 상세 페이지에서는 모든 필드 필요
article = Article.objects.get(id=article_id)
return render(request, 'articles/detail.html', {'article': article})
# 파일 업로드가 있는 모델의 경우
class Document(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
file_content = models.BinaryField() # 큰 바이너리 데이터
uploaded_at = models.DateTimeField(auto_now_add=True)
# 파일 내용 제외하고 목록 조회
def document_list():
return Document.objects.defer('file_content').all()
복합 최적화 전략
# defer()와 select_related() 조합
def optimized_article_with_author():
return Article.objects.select_related('author').defer(
'content', # 큰 텍스트 필드 제외
'author__bio' # 작성자 바이오도 제외
)
# 조건부 defer() 사용
def conditional_defer(include_content=False):
queryset = Article.objects.select_related('author')
if not include_content:
queryset = queryset.defer('content')
return queryset
⚡ 5. 성능 비교 및 분석
메모리 사용량 비교
import sys
from django.test.utils import override_settings
@override_settings(DEBUG=True)
def memory_usage_comparison():
# 테스트 데이터 생성
test_count = 1000
# 1. 전체 객체 로드
full_objects = list(Article.objects.all()[:test_count])
full_memory = sys.getsizeof(full_objects) + sum(
sys.getsizeof(obj) for obj in full_objects
)
# 2. values() 사용
values_data = list(Article.objects.values(
'title', 'view_count'
)[:test_count])
values_memory = sys.getsizeof(values_data) + sum(
sys.getsizeof(item) for item in values_data
)
# 3. values_list() 사용
values_list_data = list(Article.objects.values_list(
'title', 'view_count'
)[:test_count])
values_list_memory = sys.getsizeof(values_list_data) + sum(
sys.getsizeof(item) for item in values_list_data
)
# 4. only() 사용
only_objects = list(Article.objects.only(
'title', 'view_count'
)[:test_count])
only_memory = sys.getsizeof(only_objects) + sum(
sys.getsizeof(obj) for obj in only_objects
)
print("=== 메모리 사용량 비교 ===")
print(f"전체 객체: {full_memory:,} bytes")
print(f"values(): {values_memory:,} bytes ({values_memory/full_memory*100:.1f}%)")
print(f"values_list(): {values_list_memory:,} bytes ({values_list_memory/full_memory*100:.1f}%)")
print(f"only(): {only_memory:,} bytes ({only_memory/full_memory*100:.1f}%)")
실행 시간 벤치마크
import time
from django.db import connection, reset_queries
def benchmark_methods():
test_queries = [
("Full objects", lambda: list(Article.objects.all()[:1000])),
("values()", lambda: list(Article.objects.values('title', 'view_count')[:1000])),
("values_list()", lambda: list(Article.objects.values_list('title', 'view_count')[:1000])),
("only()", lambda: list(Article.objects.only('title', 'view_count')[:1000])),
("defer()", lambda: list(Article.objects.defer('content', 'summary')[:1000])),
]
results = []
for name, query_func in test_queries:
reset_queries()
start_time = time.time()
# 3번 실행해서 평균 계산
times = []
for _ in range(3):
start = time.time()
query_func()
times.append(time.time() - start)
avg_time = sum(times) / len(times)
query_count = len(connection.queries)
results.append({
'method': name,
'avg_time': avg_time,
'query_count': query_count
})
print("=== 성능 벤치마크 결과 ===")
for result in results:
print(f"{result['method']:15}: {result['avg_time']:.4f}초, {result['query_count']}개 쿼리")
🎯 6. 실무 적용 가이드
방법별 선택 기준
# 1. API 응답 최적화
class ArticleViewSet(viewsets.ModelViewSet):
def list(self, request):
# values() 사용 - JSON 직렬화에 최적
articles = Article.objects.filter(
is_published=True
).values(
'id', 'title', 'summary', 'author__name', 'published_date'
)
return Response({'articles': list(articles)})
def retrieve(self, request, pk=None):
# only() 사용 - 모델 메서드 활용 가능
article = Article.objects.select_related('author').only(
'title', 'content', 'author__name', 'published_date'
).get(pk=pk)
# 모델 메서드 사용 가능
return Response({
'article': {
'title': article.title,
'content': article.content,
'author': article.author.name,
'word_count': article.get_word_count(), # 모델 메서드
}
})
# 2. 대시보드 데이터 준비
def dashboard_analytics():
# values() + aggregation
author_stats = Article.objects.values('author__name').annotate(
total_articles=Count('id'),
total_views=Sum('view_count'),
avg_views=Avg('view_count'),
latest_article=Max('published_date')
).order_by('-total_views')
return list(author_stats)
# 3. 데이터 내보내기
def export_to_csv():
# values_list() 사용 - CSV 작성에 최적
data = Article.objects.filter(
is_published=True
).values_list(
'title', 'author__name', 'published_date', 'view_count'
)
response = HttpResponse(content_type='text/csv')
writer = csv.writer(response)
writer.writerow(['제목', '작성자', '발행일', '조회수'])
writer.writerows(data)
return response
성능 모니터링
# Django Debug Toolbar와 함께 사용
def monitor_query_performance(view_func):
def wrapper(*args, **kwargs):
from django.db import connection
from django.conf import settings
if settings.DEBUG:
initial_queries = len(connection.queries)
start_time = time.time()
result = view_func(*args, **kwargs)
end_time = time.time()
final_queries = len(connection.queries)
print(f"함수: {view_func.__name__}")
print(f"실행 시간: {end_time - start_time:.4f}초")
print(f"DB 쿼리: {final_queries - initial_queries}개")
return result
return view_func(*args, **kwargs)
return wrapper
# 사용 예제
@monitor_query_performance
def optimized_article_list():
return Article.objects.select_related('author').only(
'title', 'summary', 'published_date', 'author__name'
)[:50]
📊 7. 고급 최적화 패턴
Prefetch와 조합
from django.db.models import Prefetch
# 복잡한 관계 최적화
def advanced_optimization():
authors = Author.objects.prefetch_related(
Prefetch(
'article_set',
queryset=Article.objects.filter(is_published=True).only(
'title', 'view_count', 'published_date'
).order_by('-published_date')
)
).only('name', 'email')
for author in authors:
print(f"작성자: {author.name}")
for article in author.article_set.all():
print(f" - {article.title} ({article.view_count}회)")
# 조건부 최적화
def conditional_optimization(user_role):
queryset = Article.objects.select_related('author')
if user_role == 'admin':
# 관리자는 모든 필드 접근 가능
return queryset
elif user_role == 'editor':
# 에디터는 content 제외
return queryset.defer('content')
else:
# 일반 사용자는 제목과 요약만
return queryset.only('title', 'summary', 'author__name')
캐싱과 연계
from django.core.cache import cache
def cached_article_list():
cache_key = 'article_list_optimized'
articles = cache.get(cache_key)
if articles is None:
# values()로 캐시하기 좋은 형태로 변환
articles = list(Article.objects.filter(
is_published=True
).values(
'id', 'title', 'summary', 'author__name', 'published_date'
).order_by('-published_date')[:20])
cache.set(cache_key, articles, 300) # 5분 캐시
return articles
🎯 결론 및 권장사항
언제 어떤 방법을 사용할까?
| 상황 | 권장 방법 | 이유 |
|---|---|---|
| API JSON 응답 | values() |
딕셔너리 → JSON 변환 용이 |
| CSV/Excel 내보내기 | values_list() |
튜플 → 행 데이터 최적 |
| 템플릿 렌더링 | only() |
모델 메서드 사용 가능 |
| 대용량 텍스트 제외 | defer() |
선택적 필드 로딩 |
| 단순 통계 계산 | values_list(flat=True) |
메모리 효율 최고 |
성능 최적화 체크리스트
- 필요한 필드만 선택: 불필요한 필드 로딩 방지
- 관계 최적화:
select_related(),prefetch_related()활용 - 인덱스 활용: 필터링 필드에 적절한 인덱스 설정
- 지연 로딩 주의:
only(),defer()사용 시 추가 쿼리 발생 가능성 고려 - 캐싱 전략: 자주 사용되는 데이터는 캐시 활용
실무 권장사항
# 좋은 예시
def good_optimization():
# 목적에 맞는 최적화
return Article.objects.select_related('author').values(
'id', 'title', 'summary', 'author__name', 'published_date'
).filter(is_published=True).order_by('-published_date')
# 피해야 할 예시
def bad_optimization():
# N+1 문제 발생 위험
articles = Article.objects.only('title') # author 정보 없음
for article in articles:
print(article.author.name) # 매번 추가 쿼리!
Django의 QuerySet 최적화 메서드들을 적절히 활용하면 애플리케이션의 성능을 크게 개선할 수 있습니다. 각 방법의 특성을 이해하고 상황에 맞게 선택하여 사용하세요.
더 자세한 내용은 Django 공식 문서를 참조하시기 바랍니다.