Django ORM 최적화가 중요한 이유
Django ORM을 사용하면서 빈번히 마주하는 N+1 쿼리 문제 해결
N+1 문제
메인 쿼리 1번과 관련 객체를 가져오는 N번의 추가 쿼리가 실행되는 현상
1
2
3
4
5
6
7
8
9
10
11
| class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# 문제가 되는 코드
books = Book.objects.all() # 1번의 쿼리
for book in books:
print(book.author.name) # 각 book마다 1번씩 추가 쿼리 = N번의 쿼리
|
실제 실행되는 SQL
1
2
3
4
5
6
7
8
| -- 첫 번째 쿼리: 모든 책 조회
SELECT * FROM book;
-- 각 책마다 실행되는 쿼리들
SELECT * FROM author WHERE id = 1;
SELECT * FROM author WHERE id = 2;
SELECT * FROM author WHERE id = 3;
-- ... 책의 개수만큼 반복
|
Django 내에서 문제 확인 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
| from django.db import connection
# 쿼리 실행 전 초기화
connection.queries.clear()
books = Book.objects.all()
for book in books:
print(book.author.name)
# 실행된 쿼리 확인
print(f"실행된 쿼리 수: {len(connection.queries)}")
for query in connection.queries:
print(query['sql'])
|
select_related()
는 ForeignKey
나 OneToOneField
관계에서 관련 객체를 JOIN을 통해 한 번에 불러옴
1
2
| # N+1 문제 해결
books = Book.objects.select_related('author').all()
|
실제 실행되는 SQL
1
2
3
4
5
6
| -- 단 1번의 쿼리로 해결
SELECT
book.id, book.title, book.author_id,
author.id, author.name
FROM book
INNER JOIN author ON book.author_id = author.id;
|
중첩 관계에서의 활용
1
2
3
4
5
6
7
8
9
10
| class Publisher(models.Model):
name = models.CharField(max_length=100)
class Author(models.Model):
name = models.CharField(max_length=100)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
books = Book.objects.select_related('author__publisher').all()
for book in books:
print(f"{book.title} by {book.author.name} ({book.author.publisher.name})")
|
prefetch_related()
는 ManyToManyField
나 역참조 관계
에서 별도의 쿼리로 관련 객체들을 미리 불러옴
ManyToManyField
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class Category(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
categories = models.ManyToManyField(Category)
# N+1 문제
books = Book.objects.all()
for book in books:
categories = book.categories.all() # 각 book마다 쿼리 실행
print(f"{book.title}: {[c.name for c in categories]}")
# 해결
books = Book.objects.prefetch_related('categories').all()
for book in books:
categories = book.categories.all() # 캐시된 데이터 사용
print(f"{book.title}: {[c.name for c in categories]}")
|
역참조 관계
1
2
3
4
| authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
books = author.book_set.all() # 추가 쿼리 없음
print(f"{author.name}: {[b.title for b in books]}")
|
실행되는 SQL
1
2
3
4
5
6
7
8
9
| -- 첫 번째 쿼리: 메인 객체들
SELECT * FROM book;
-- 두 번째 쿼리: 관련 객체들을 한 번에
SELECT * FROM category
WHERE id IN (
SELECT category_id FROM book_category
WHERE book_id IN (1, 2, 3, 4, 5...)
);
|
Prefetch 객체로 고급 제어하기
Prefetch 객체
를 사용하면 prefetch_related()
에서 필터링이나 정렬을 적용 가능
Prefetch
1
2
3
4
5
6
7
8
9
| from django.db.models import Prefetch
books = Book.objects.prefetch_related(
Prefetch('categories', queryset=Category.objects.filter(is_active=True))
).all()
for book in books:
active_categories = book.categories.all()
print(f"{book.title}: {[c.name for c in active_categories]}")
|
to_attr로 별도 속성에 저장
1
2
3
4
5
6
7
8
9
10
| books = Book.objects.prefetch_related(
Prefetch(
'categories',
queryset=Category.objects.filter(is_active=True),
to_attr='active_categories'
)
).all()
for book in books:
print(f"{book.title}: {[c.name for c in book.active_categories]}")
|
복잡한 조건의 Prefetch
1
2
3
4
5
6
7
8
9
10
11
12
| from django.db.models import Q
# 복잡한 조건과 정렬 적용
books = Book.objects.prefetch_related(
Prefetch(
'categories',
queryset=Category.objects.filter(
Q(is_active=True) & Q(created_date__gte='2023-01-01')
).order_by('name'),
to_attr='recent_active_categories'
)
).all()
|
only() & defer()
큰 텍스트 필드나 불필요한 필드를 제외하여 네트워크 트래픽과 메모리 사용량 최적화 가능
only() - 특정 필드만 가져오기
1
2
3
4
5
| books = Book.objects.select_related('author').only('title', 'author__name')
for book in books:
print(f"{book.title} by {book.author.name}")
# 다른 필드에 접근하면 추가 쿼리 발생
|
defer() - 특정 필드 제외하고 가져오기
1
2
3
4
5
6
| # 큰 텍스트 필드는 제외하고 가져오기
books = Book.objects.defer('content', 'description')
for book in books:
print(book.title) # 추가 쿼리 없음
# print(book.content) # 쿼리 발생
|
실행되는 SQL
1
| books = Book.objects.select_related('author').only('title', 'author__name')
|
1
2
3
4
| -- only()로 지정한 필드만 SELECT
SELECT book.title, author.name
FROM book
INNER JOIN author ON book.author_id = author.id;
|
Lazy Loading
Django ORM은 지연 로딩(Lazy Loading) 사용 (실제로 데이터가 필요한 시점에서 쿼리가 실행)
지연 로딩 동작 방식
1
2
3
4
5
6
7
8
| # 이 시점에서는 쿼리가 실행되지 않음
books = Book.objects.all()
filtered_books = books.filter(title__icontains='python')
sorted_books = filtered_books.order_by('title')
# 실제로 데이터가 필요한 시점에서 쿼리 실행
for book in sorted_books: # 이 시점에서 쿼리 실행!
print(book.title)
|
즉시 실행 방법들
1
2
3
4
5
6
7
8
9
10
11
| # 방법 1: list() 사용
books = list(Book.objects.all())
# 방법 2: len() 사용
count = len(Book.objects.all())
# 방법 3: 슬라이싱
first_10 = Book.objects.all()[:10]
# 방법 4: bool() 사용
has_books = bool(Book.objects.all())
|