-
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_related
와prefetch_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", ... }
다양한 조건의 다양한 출력을 제공할 수 있습니다.
'기술 > Django' 카테고리의 다른 글
프로덕션의 마이그레이션은 검토가 필요합니다 (0) 2024.01.18 일반적인 Django 서비스는 단일 앱을 통해 개발해야합니다. (0) 2022.12.11 Django Application 의 메모리 누수 해결하기 (0) 2022.01.25 'Settings' object has no attribute 'worker_state_db' (0) 2021.09.25 Django Rest Framework 의 APITestCase 에서 API 를 요청할 때 Body 데이터를 보내는 두가지 방법 (0) 2021.09.24