Django ORM 쿼리 기본 최적화

Django ORM 쿼리 기본 최적화

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()ForeignKeyOneToOneField 관계에서 관련 객체를 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())
This post is licensed under CC BY 4.0 by the author.