본문 바로가기
Django

[Django] select_related(), prefetch_related()란?(N+1문제 해결법)

by kyung-mini 2024. 9. 19.

Django 로고
django 로고

1. 개요

본 포스팅에서는 N+1문제가 무엇인지에 대해 알아보겠습니다. 이후 select_relatedprefetch_related메서드란 무엇인지 다루고, 이를 활용하여 N+1문제를 해결하는 방법에 대해 알아보겠습니다.

 

지난번 포스팅에서는 annotate메서드에 대해서 다루었습니다. 해당 메서드를 공부는 과정에서  select_ralatedprefetch_related메서드를 접하게 되었습니다. 따라서, 이번 포스팅을 작성하게 되었습니다.

 

annotate에 대한 추가적인 정보는 아래 링크를 통하시면 됩니다.

[Django] annotate()란? - annotate 사용법(with. only(),values() 쿼리 최적화)

 

2. N+1 문제란?

select_relatedprefetch_related 모두 N+1문제를 해결하는 데 유용한 메서드입니다.

 

여기서 N+1 문제란 N개의 항목을 조회할 때, 각 항목에 대해 추가적인 N의 쿼리가 발생할 때를 일컷습니다. 예를 들어, N개의 객체를 가져오기 위해 한 번 쿼리가 발생하고, 각 개체에 대한 데이터를 조회하기 위해 N번의 쿼리가 발생하는 상황입니다.

 

이러한 N+1문제가 발생하면 불필요한 DB히트가 많아지기 때문에 성능이 저하됩니다. 이를 최적화하기 위한 메서드가 select_related prefetch_related입니다.

 

N+1 문제에 대해서 알아보았으니 select_relatedprefetch_related를 알아보고 이를 통해 N+1 문제를 해결하는 방법에 대해서 알아보겠습니다.

3. select_related란?

select_related를 사용하면 현재 모델과 현재 모델에 연결된 객체를 하나의 쿼리로 결합하여 가져옵니다. 이때 연결된 객체는 OneToOneField이거나, ForeignKey정참조이어야 합니다.즉, select_related단일 객체와의 관계에 사용됩니다.

 

간단하게 정리하자면 현재 모델과 연결된 객체를 한 번에 가져옵니다. 이때 연결된 객체는 단일 객체여야 합니다.

원투원필드는 양방향 가능하지만, 외래키는 정참조일 때만 가능
select_related를 사용할 수 있는 모델 관계

3.1 예시 모델과 테이블

이번에 사용하게 될 예시의 모델과 데이터 테이블입니다.

# models.py
class Category(models.Model):
    name = models.CharField(max_length=100)

class Drink(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

 

Category 테이블

id name
1 Soft Drink
2 Juice
3 Alcoholic Beverage

 

Drink 테이블

id name category_id
1 Coca Cola 1
2 Orange Juice 2
3 Beer 3
4 Pepsi 1

 

3.2 N+1 문제 발생 예시

# N+1 문제 발생 예시
drinks = Drink.objects.all()  # 1개의 쿼리 🌐

for drink in drinks:
    print(f"Drink: {drink.name}, Category: {drink.category.name}")  # N개의 추가 쿼리 발생 🌐

 

위 코드를 실행하면 아래와 같은 출력결과가 나옵니다.

Drink: Coca Cola, Category: Soft Drink    🌐
Drink: Orange Juice, Category: Juice      🌐
Drink: Beer, Category: Alcoholic Beverage 🌐
Drink: Pepsi, Category: Soft Drink        🌐

 

Drink 모델의 객체를 가져오는데 한 번의 DB히트가 발생합니다. 이후 반복문을 순회하면서 4번의 추가 DB 히트가 발생합니다. 총 (1 + 4)번의 DB 히트가 발생하는 N+1문제가 발생합니다.

(🌐는 DB히트가 발생함을 의미합니다.)

 

3.3 select_related 사용하기

select_related를 사용하여 DrinkCategory데이터를 한 번에 가져옵니다.

drinks = Drink.objects.select_related('category').all()

 

아래와 같은 쿼리가 반환됩니다. 

Drink ID Drink Name Category ID Category Name
1 Coca Cola 1 Soft Drink
2 Orange Juice 2 Juice
3 Beer 3 Alcoholic Beverage
4 Pepsi 1 Soft Drink

 

sql에 익숙하신 분들이라면 비슷한 것이 떠오르리라 생각합니다.

select_related는 SQL의 INNER JOIN방식으로 쿼리를 최적화 하는 방법입니다.

 

4. prefetch_related()란?

prefetch_related를 사용하면 한 번에 여러 쿼리를 가져옵니다. 가져온 쿼리는 메모리에 저장하고, 필요할 때 이를 사용합니다. 이를 통해 성능을 최적화합니다.

 

이 메서드는 ManyToManyField이거나 Reverse ForeignKey에서 사용하는 것이 보편적입니다. 이 메서드는 모든 관계에서 사용할 수 있지만, 권장하지는 않습니다. 단일 객체에는 prefetch_related가 일반적으로 효율적이지 않기 때문입니다.

원투원 필드, 외래키 모두에서 사용 가능함
prefetch_related를 사용할 수 있는 모델 관계

 

위에서 설명하였듯이 일반적으로 외래키의 정참조일 경우는 select_related사용을 권장합니다. 하지만, 특정 상황에서는 prefetch_related를 사용합니다.

아래의 코드는 외래키 정참조이지만, prefetch_related를 사용하는 예시입니다.

class Category(models.Model):
    name = models.CharField(max_length=100)

class Manufacturer(models.Model):
    name = models.CharField(max_length=100)

class Drink(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)

# 복잡한 관계를 효율적으로 가져오기 위해 prefetch_related() 사용
drinks = Drink.objects.prefetch_related('category', 'manufacturer').all()

 

위와 같은 경우에는 select_related를 사용하여 Category, Manufacturer 모델의 데이터를 가져오면 중복이 발생합니다. 그러나, prefetch_related를 사용하면 두 모델의 데이터를 별도의 쿼리로 가져오기 때문에 중복이 발생하지않습니다. 이와 같이 select_related보다 prefetch_related를 사용하는 것이 효율적인 상황도 있습니다.

4.1 N+1문제 발생 예시

prefetch_related를 사용하지 않았을 떄 발생하는 N+1문제 상황입니다.

# N+1 문제 발생 코드
categories = Category.objects.all()  # 1개의 쿼리로 카테고리 데이터를 가져옴 🌐

for category in categories:
    print(f"Category: {category.name}")
    for drink in category.drinks.all():  # N개의 추가 쿼리 발생 🌐
        print(f"  - Drink: {drink.name}")

 

위 코드를 실행시키면 아래와 같은 출력 결과를 얻을 수 있습니다.

Category: Soft Drink         🌐
  - Drink: Coca Cola
  - Drink: Pepsi
Category: Juice              🌐
  - Drink: Orange Juice
Category: Alcoholic Beverage 🌐
  - Drink: Beer

 

Category의 객체를 가져올 때 한 번의 DB 히트가 발생합니다. 추가적으로 반복문을 순회하면서 3번의 DB히트가 발생합니다. 총 (1+3)번의 DB히트가 발생하는 N+1문제 상황입니다.

(🌐는 DB히트가 발생함을 의미합니다.)

 

4.2 사용 예시

코드를 아래와 같이 수정하면 두 번의 DB 히트만이 발생합니다. 한 번은 Category의 데이터를 가져올 때 발생하고, 나머지 한 번은 연결되어있는 Drink의 데이터를 가져올 때 발생합니다.

categories = Category.objects.prefetch_related('drinks').all()

 

categories에는 아래와 같은 구조로 데이터가 들어가게 됩니다.

Categories
    Category 1 (Soft Drink):
        drinks: Drink.objects.filter(category_id=1) → Coca Cola, Pepsi
        
    Category 2 (Juice):
        drinks: Drink.objects.filter(category_id=2) → Orange Juice
        
    Category 3 (Alcoholic Beverage):
        drinks: Drink.objects.filter(category_id=3) → Beer

 

5. select_related vs prefectch_related

  select_related() prefetch_related()
사용 목적 ForeignKey 또는 OneToOne 관계에서 관련 객체를 즉시 로드 ManyToMany, 역참조 또는 ForeignKey 관계에서 데이터를 미리 로드
쿼리 방식 JOIN을 사용하여 하나의 쿼리로 데이터를 가져옴 여러 개의 쿼리를 실행한 후, Python에서 데이터를 결합
DB 히트 수 1번 2번 이상
성능 최적화 단일 객체 관계(ForeignKey, OneToOne)에 적합 다중 객체 관계(ManyToMany, 역참조)에 적합
데이터 결합 방식 데이터베이스에서 JOIN을 통해 즉시 결합 Python 메모리에서 결합
적용 방식 즉시 로딩 (Eager loading) Lazy loading과 유사하지만, 미리 데이터를 로드 후 결합
예시 사용 상황 음료가 속해있는 카테고리 표시 카테고리에 있는 여러 음료 표시할  때 사용

 

6. 마치며

오늘은 N+1문제, select_related, prefetch_related에 대해서 알아보았습니다.

 

N+1문제는 각 객체에 대해 추가적인 쿼리가 발생하여 성능 저하를 일으키는 문제입니다. select_relatedprefetch_related를 사용한다면 쿼리를 최적화할 수 있습니다.

 

오늘 다룬 주제는 면접에서 단골 질문 중 하나라고 하니, 추가적으로 알아보셔도 좋을 것 같습니다.

 

 

 

[참조]

 

 

QuerySet API reference | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

Django selected_related, prefetch_related 면접 단골 질문

Django에서 N+1 문제를 해결하는 방법을 알아 봅니다. 바로 select_related, prefetch_related 입니다. 이번 포스팅에서는 이 두 메소드에 대해서 알아보겠습니다.

chaechae.life

 

(Django) select_related 와 prefetch_related를 사용한 데이터 참조

셀렉트할 객체가 역참조하는 single object(one-to-one or many-to-one)이거나, 또는 정참조 foreign key 일 때 사용한다. You can use related_name in the linking model for backward re

velog.io