1. 개요
본 포스팅에서는 N+1문제가 무엇인지에 대해 알아보겠습니다. 이후 select_related
와 prefetch_related
메서드란 무엇인지 다루고, 이를 활용하여 N+1문제를 해결하는 방법에 대해 알아보겠습니다.
지난번 포스팅에서는 annotate
메서드에 대해서 다루었습니다. 해당 메서드를 공부는 과정에서 select_ralated
와 prefetch_related
메서드를 접하게 되었습니다. 따라서, 이번 포스팅을 작성하게 되었습니다.
annotate
에 대한 추가적인 정보는 아래 링크를 통하시면 됩니다.
[Django] annotate()란? - annotate 사용법(with. only(),values() 쿼리 최적화)
2. N+1 문제란?
select_related
와 prefetch_related
모두 N+1문제를 해결하는 데 유용한 메서드입니다.
여기서 N+1 문제란 N개의 항목을 조회할 때, 각 항목에 대해 추가적인 N의 쿼리가 발생할 때를 일컷습니다. 예를 들어, N개의 객체를 가져오기 위해 한 번 쿼리가 발생하고, 각 개체에 대한 데이터를 조회하기 위해 N번의 쿼리가 발생하는 상황입니다.
이러한 N+1문제가 발생하면 불필요한 DB히트가 많아지기 때문에 성능이 저하됩니다. 이를 최적화하기 위한 메서드가 select_related
와 prefetch_related
입니다.
N+1 문제에 대해서 알아보았으니 select_related
와 prefetch_related
를 알아보고 이를 통해 N+1 문제를 해결하는 방법에 대해서 알아보겠습니다.
3. select_related란?
select_related
를 사용하면 현재 모델과 현재 모델에 연결된 객체를 하나의 쿼리로 결합하여 가져옵니다. 이때 연결된 객체는 OneToOneField
이거나, ForeignKey
의 정참조이어야 합니다.즉, 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
를 사용하여 Drink
와 Category
데이터를 한 번에 가져옵니다.
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
가 일반적으로 효율적이지 않기 때문입니다.
위에서 설명하였듯이 일반적으로 외래키의 정참조일 경우는 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_related
와 prefetch_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
'Django' 카테고리의 다른 글
[Django] annotate()란? - annotate 사용법(with. only(),values() 쿼리 최적화) (0) | 2024.09.13 |
---|---|
[Django] F객체, Q객체란? - ORM에서 F,Q 사용하기(짤막팁 2편) (0) | 2024.09.08 |
[Django] Signals 사용법 - 프로필 자동 생성, 좋아요 숫자 갱신(signal 표로 정리) (2) | 2024.09.05 |