ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Django 에서 N+1 쿼리 문제 예방, 발견하기
    기술/Django 2022. 1. 25. 22:18

    N+1 이란?

    쿼리를 한번으로 N건 가져왔는데, 관련 컬럼을 얻기 위해 N번의 쿼리를 추가수행하는 문제는 N+1 문제라고 합니다.

    간단한 예시를 통해 N+1 이 발생하는 원리를 알아봅시다.

    class PressGroup(models.Model):
        name = models.CharField(max_length=64)
    
    
    class Reporter(models.Model):
        full_name = models.CharField(max_length=64)
        press_group = models.ForeignKey(PressGroup, on_delete=models.CASCADE)
    
    
    class Article(models.Model):
        headline = models.CharField(max_length=64)
        content = models.TextField()
        reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)

     

    예시 1. Article 목록 출력하기 ( 최적화 X )

    for article in Article.objects.all():
        # 여기서 article 과 연결된 reporter 정보를 가져오기 위해 추가적인 쿼리가 발생합니다.
        reporter_full_name = article.reporter.full_name
        print(f'{article.headline}, {reporter_full_name}')

    즉, 위의 예시에서는 article 의 수만큼 reporter 를 가져오는 쿼리가 발생합니다.

    여러가지 형태로 관계를 가진 데이터를 가져오려고 하면 모두 쿼리가 발생합니다.

     

    예시 2. 언론사의 Article 출력하기 ( 최적화 X )

    for press_group in PressGroup.objects.all():
        # reporter_set 의 데이터 하나하나 가져올 때마다 쿼리가 발생합니다.
        for reporter in press_group.reporter_set.all():
            # article_set 의 데이터 하나하나 가져올 때마다 쿼리가 발생합니다.
            for article in reporter.article_set.all():
                print(f'{article.headline}, {reporter.full_name}')

    해결 방법

    Django 는 이런 문제들을 해결하기 위해 select_relatedprefetch_related 라는 메서드를 마련해두었습니다.

    두 메서드는 유사하게 동작합니다. 둘 다 관련된 데이터를 가져오기 위해 사용됩니다.

    다른 점은 select_related 는 동일한 쿼리에서 Join 하여 가져옵니다. prefetch_related 는 다른 쿼리로 만들어지고 결과가 Django 에서 합쳐집니다.

    위의 두 예시는 아래와같이 해결될 수 있습니다.

     

    예시 1. Article 목록 출력하기 ( 최적화 O )

    for article in Article.objects.all().select_related('reporter'):  # 여기서 Join 되어
        # 이제 더이상 쿼리가 발생하지 않습니다.
        reporter_full_name = article.reporter.full_name
        print(f'{article.headline}, {reporter_full_name}')

     

    예시 2. 언론사의 Article 출력하기 ( 최적화 O )

    for press_group in PressGroup.objects.all().prefetch_related('reporter_set'):  # 여기서 reporter 목록을 가져와서 각 press_group 에 데이터를 합쳐놓습니다.
        for reporter in press_group.reporter_set.all().prefetch_related('article_set'):  # 여기서 article 목록을 가져와서 각 reporter 에 합쳐놓습니다.
            for article in reporter.article_set.all():
                print(f'{article.headline}, {reporter.full_name}')

    예방하거나 발견할 수 있는 방법

    여기서는 django-query-capture 라는 라이브러리를 사용하여 예방, 발견하는 방법을 소개합니다.

    pip install django-query-capture

    예방법

    각 API, 함수를 django-query-capture 에서 제공해주는 모듈을 사용하여 테스트를 작성하여 예방할 수 있습니다.

    만약 최적화 하지 않은 쿼리 문을 쓴 코드가 있다면 정리된 에러 로그와 함께 에러가 발생합니다. 만약 최적화 성공했다면 출력 내용 없이 그냥 넘어갑니다.

     

    예시 1. Article 목록 출력하기 ( 최적화 X )

    with AssertInefficientQuery(num=1):  # 2개 이상 쿼리 중복, 유사 시 에러 발생
        for article in Article.objects.all():
            reporter_full_name = article.reporter.full_name
            print(f'{article.headline}, {reporter_full_name}')

    에시 2 도 마찬가지로 최적화 하지 않은 부분에 대해서 에러 로그와 함께 에러가 발생합니다.

     

    예시 2. 언론사의 Article 출력하기 ( 최적화 X )

    with AssertInefficientQuery(num=1):  # 2개 이상 쿼리 중복, 유사 시 에러 발생
        for press_group in PressGroup.objects.all():
            for reporter in press_group.reporter_set.all():
                for article in reporter.article_set.all():
                    print(f'{article.headline}, {reporter.full_name}')

    발견법

    django-query-capture 에는 모든 HTTP Request 의 쿼리 현황을 조사할 수 있는 Middleware 를 제공합니다.

    MIDDLEWARE = [
      ...,
      "django_query_capture.middleware.QueryCaptureMiddleware",
    ]

    Middleware 를 넣은 후 HTTP Request 를 몇번 해보면, HTTP Request 마다 쿼리 내용이 정리되어 나옵니다.

    만약 출력을 변경하고 싶으면 커스터마이징 기능도 제공합니다. 공식 문서에 어떤 Presenter 가 있는지,

    어떻게 커스터마이징 할 수 있는지 자세히 나와있습니다.

    QUERY_CAPTURE = {
        "PRESENTER": "django_query_capture.presenter.PrettyPresenter",
        ...
    }

    다양한 조건의 다양한 출력을 제공할 수 있습니다.

    댓글

Designed by Tistory.