본문 바로가기
Coding/Python

"N + 1 문제" 현상과 Django 에서 select_related함수

by 우지uz 2023. 12. 8.

def select_related(self, *fields: Any) -> Self: ...https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

 

N+1 문제 - Incheol's TECH BLOG

Query를 실행하도록 지원해주는 다양한 플러그인이 있다. 대표적으로 Mybatis, QueryDSL, JOOQ, JDBC Template 등이 있을 것이다. 이를 사용하면 로직에 최적화된 쿼리를 구현할 수 있다.

incheol-jung.gitbook.io

 

N + 1 문제

언제 ??
주로 외래키(혹은 다대다)와 같은 관계를 갖는 데이터베이스 모델에서 발생한다고 합니다.

"N+1 문제"란?
연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에
조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이를 N+1 문제라고 한다.

예를 들어, Product 모델에서 Category 모델을 ForeignKey로 가지고 왔을 때 

class Category(models.Model):
    class Meta:
        verbose_name = verbose_name_plural = "상품 분류"

    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return f"{self.name}"



class Product(models.Model):
    class Status(models.TextChoices):
        ACTIVE = "active", "활성화"
        SOLDOUT = "soldout", "품절"
        OBSOLETE = "obsolete", "단종"
        INACTIVE = "inactive", "비활성화"
    category = models.ForeignKey(Category, on_delete=models.CASCADE, db_constraint=False) # db_constraint=False 는 외래키 제약조건을 걸지 않겠다는 의미
    name = models.CharField(max_length=100, db_index=True)
    description = models.TextField(blank=True)
    price = models.PositiveIntegerField() # 0 이상의 정수
    status = models.CharField(max_length=10, choices=Status.choices, default=Status.INACTIVE)
    photo = models.ImageField(blank=True, upload_to="mall/product/photo/%Y/%m/%d")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

프론트에서 product.category.name 로 카테고리 이름을 가지고 오려면

Product 갯수만큼 Category를 참조해서 가져와야 합니다.
< 상품 갯수가 369개면, 카테고리 이름을 요청하는 것도 369개 >

실제로 백엔드 Views.py 에서 작성한 Product.objects.all() 를 사용한 
def product_list(request): 함수를 
프론트에서 urls와 함께 데이터를 가져오게 된다면

다음과 같이, SQL - 369개의 상품에 대해서 쿼리들이 발생했고, 시간은 7.34ms임음을 알 수 있습니다.

{% for product in product_list %}
    <div class="col-sm-6 col-lg-4">
        <div class="card">
            <img src="{{ product.photo.url }}"
                 alt="상품 사진"
                 class="card-img-top object-fit-cover">
            <div class="card-body">
                <p>번호 : {{ product.id }}</p>
                <p>상품 종류 : {{ product.category.name }}</p>
                <div class="text-truncate">
                    <h5>{{ product.name }}</h5>
                </div>
                <div class="d-flex justify-content-between">
                    <div>{{ product.price|intcomma }}원</div>
                    <div>
                        <a href="#" class="btn btn-primary">장바구니</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endfor %}

프론트는
장고 템플릿 문법에서 for in 문법을 통해 
상품 모델 정보를 불러오고 있는데요 

여기서 반복문 for .. in .. :을 통해 
<p>상품 종류 : {{ product.category.name }}</p>
상품의 카테고리 이름을 불러옵니다

여기서 Product 를 주 엔터티(Dominant Entity, Independent Entity)
Category 를 종속 엔터티(Dependent Entity, Subordinate Entity)
라고 합니다
(https://dataprofessional.tistory.com/17)

 

장고에서는
select_related 혹은 prefetch_related
라는 장고의 기능을 사용해서,
추가적인 쿼리를 최소화하고 성능을 향상시킬 수 있습니다. 

def product_list(request):
    # products_qs = Product.objects.all()
    products_qs = Product.objects.all().select_related("category")
    # Product 에서 Category를 외래키로 참조해서 가져오는 것은, 데이터베이스에 쿼리를 두번 요청하는 것이므로 비효율적입니다. 이를 해결하기 위해 select_related() 메서드를 사용합니다.
    return render(
        request,
        'mall/product_list.html',
        {
            "product_list": products_qs,
        },
    )

.select_related("category") 를 통해서 category 필드에 대해서 
Join 하게 되었고 

def select_related(self, *fields: Any) -> Self: ...
인자로 fields 필드들을 받는 것을 확인할 수 있습니다. 

이는 불필요한 반복된 쿼리를 피하고 데이터를 효율적으로 로드하여
N+1 문제를 해결하는 데 도움이 됩니다.

 

select_related 은 ForeignKey와 OneToOneField와 같은 관계 필드에서 사용되고

prefetch_related은 ManyToManyField 및 GenericRelation과 같은 "다대다 및 역참조 "관계에서 사용됩니다.

prefetch_related의 사용도 다음과 같습니다. 

# 예시
products = Product.objects.all().prefetch_related('categories')

사실 어떤 관계의 필드인지는 데이터 모델링 마다 다를 것이고, 

프론트에서 어떤 필드를 가져올 것인지가 , 기획하는 것마다 다르기 때문에 

두 기법을 함께 이용할 수도 있고, 
따로 사용할 수도 있다고 생각합니다. 

백엔드에서 , 상품 리스트를 들고오며, 카테고리 name 뿐만아니라
다양한 데이터를 들고와야 한다면 
상품 id 에 대한 카테고리 모델에 대한 정보를 
따로 요청하고, 가지고 오는게 데이터 효율적으론 
괜찮을 수도 있을 것 같다는 생각이 듭니다. 

 

def select_related(self, *fields: Any) -> Self: ...https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

현재 자바 , 스프링을 공부하는 중에 있는데 

공부하면서 JPA 에 대해서도 더 알아가보도록 하겠습니다. 

'Coding > Python' 카테고리의 다른 글

[pyhton 기초] 동작원리? 동작 방식?  (0) 2023.04.11