당신이 몰랐던 Django Prefetch.

chrisjune
chrisjune
Published in
11 min readMay 8, 2019

prefetch_related를 효율적으로 활용하면 가독성을 높이고 성능을 개선할 수 있습니다

목차

  1. prefetch_related 연산은 쿼리가 여러번 수행된다
  2. ORM 필터 조건과 데이터의 양에 따라서 select_related 보다 prefetch_related가 성능에 유리한 경우가 있다

3. Relation(prefetch_related, select_related)를 지정하지 않고 filter에 조건을 추가하면 자동으로 inner join 질의가 수행된다

4. prefetch_related 연산결과를 to_attr 인수에 담아서 사용하자

Tip 1. Prefetch_related는 쿼리를 두번 수행한다

perfetch_related 함수는 조인을 하지 않고 개별 쿼리를 실행 후, django에서 직접 데이터 조합을 합니다.

예를 위해 Item 모델과 이를 참조하는 ItemImage 모델을 가정합니다.

Item 모델과 ItemImage ERD
# models.py
from django.db import models
class Item(models.Model):
item_no = models.BigAutoField(primary_key=True)
item_name = models.CharField()
class ItemImage(models.Model):
item_image_no = models.BigAutoField(primary_key=True)
item_no = models.ForeignKey(Item, related_name='related_item')
image_url = models.CharField()
queryset = Item.objects.prefetch_related('related_item')

위의 queryset을 실행하면,

[2019-05-08 20:05:08,224] [DEBUG] [pid: 67199] [threadid: 140734970774976] [module: django.db.backends] [/lib/python3.6/site-packages/django/db/backends/utils.py] [func: execute] [line: 91] [(0.014) SELECT * FROM "item"."t_item" DESC LIMIT 21;][2019-05-08 20:05:08,237] [DEBUG] [pid: 67199] [threadid: 140734970774976] [module: django.db.backends] [/lib/python3.6/site-packages/django/db/backends/utils.py] [func: execute] [line: 91] [(0.008) SELECT * FROM "item"."t_item_image" WHERE "item"."t_item_image"."item_no" IN (187937, 187938, 187939)]

Item 테이블에서 조건에 맞는 데이터 쿼리를 수행합니다. 그리고 item_no를 리스트로 만들냅니다.

그 후에 위의 리스트를 조건으로 ItemImage 테이블 쿼리를 수행합니다. 따라서 prefetch_related 연산은 쿼리를 최소한 두번은 수행합니다.

(filter 조건에 따라서 순서가 반대로 될 수 있습니다. ItemImage모델의 컬럼으로 조건이 들어가 있다면, ItemImage 테이블을 조회 후 Item 테이블을 조회하게 됩니다)

prefetch_related 연산이 추가되면, 그만큼 쿼리 수행도 증가합니다.

Item.objects.prefetch_related(
'item_images'
).prefetch_related(
'options'
)

Option이라는 모델이 추가되고 related_name이 options라고 할때, 위의 쿼리셋은 Item 테이블 조회 → ItemImage 테이블 조회 → Option 테이블 조회를 하여 총 세번을 수행합니다.

Tip 2. select_related를 prefetch_related로 수행하여 성능개선 할 수 있다

ItemImage.objects.select_related('item_no')

위의 쿼리셋을 수행하면 어떻게 될까요? 아시는 것 처럼 select_related 연산은 두 모델을 inner join 쿼리를 수행합니다. 즉, 쿼리를 한번만 수행합니다.

그렇다면 select_related 연산이 아닌 prefetch_related로 쿼리셋이 수행될까요?

ItemImage.objects.prefetch_related('item_no')

네 됩니다! 그런데 ItemImage 테이블을 조회하고 Item 테이블을 조회합니다.

inner join으로 한번만 수행되는 쿼리를 굳이 두번에 나눌 이유가 있을까요?

답은, 쿼리셋 조건과 데이터의 양에 따라 다릅니다. ORM 조건이 꼬여서 복잡하고, 데이터양이 많은 경우는 한번에 쿼리를 조회하는 것보다 두번에 나누어 각각 조회할 때 응답속도가 빠를 수 있습니다.

물론 어떤 조건에 따라서 성능은 달라지기 때문에, 상황에 따라서 비교가 필요합니다.

Tip 3. 복잡한 Prefetch 코드의 가독성을 높이고 성능을 개선하는 방법

  • Relation(prefetch_related, select_related)를 지정하지 않고 filter에 조건을 추가하면 inner join으로 쿼리를 한번만 수행하게 할 수 있습니다

상황에 따라서 prefetch_related를 써야만 할 때가 있습니다.

예로 rest_frameworkViewSet에서는 반환하는 QuerySet 객체가 정해져있습니다. 부모모델에서 자식모델의 필드를 접근하거나, filter를 위해서는 prefetch_related를 사용하게 됩니다.

# views.py
from rest_framework.viewsets import ModelViewSet
class ItemViewset(ModelViewSet):
def get_queryset(self):
return = Item.objects.prefetch_related('related_item')

Prefetch연산을 여러번 해야하는 경우엔 코드 가독성이 급격하게 나빠질 뿐만 아니라 쿼리 질의수도 그만큼 증가합니다.

이를 해결 하는 방법은 바로, 필터에만 조건을 추가하고 Relation을 제거하는 것 입니다.

# 기존의 queryset
Item.objects.prefetch_related(
'item_image'
)
.filter(
item_images__image_url='/example.jpg'
)
# 변경후의 queryset
Item.objects.filter(
item_image__image_url='/example.jpg'
)

Relation을 제거 후, 쿼리셋을 수행하면 어떻게 될까요?

[2019-05-08 20:45:54,628] [DEBUG] [pid: 67199] [threadid: 140734970774976] [module: django.db.backends] [/lib/python3.6/site-packages/django/db/backends/utils.py] [func: execute] [line: 91] [(3.337) SELECT * FROM "item"."t_item" INNER JOIN "item"."t_item_image" ON ("item"."t_item"."item_no" = "item"."t_item_image"."item_no") WHERE "item"."t_item_image"."image_url" = '/example.jpg'; ]

실행결과를 보면 inner join으로 한번만 질의가 수행됩니다.

그 이유는 장고 내부에서 join chain이 없으면 자동으로 inner join으로 쿼리를 하도록 강제하기 때문입니다.

왜냐하면 join chain이 없다면 필터조건이 아무리 많이 추가되어도 쿼리결과에 영향을 주지 못하기 때문입니다.

django filter 실행시 inner join로 join을 추가해주는 함수

이를 통한 장점은

첫번째로, 일반적으로 성능을 개선 할 수 있습니다. prefetch_related 연산은 쿼리가 여러번 수행되고 장고에서 합치는 연산절차를 거칩니다. 그래서 여러번의 쿼리 질의를 한번으로 줄일 수 있는 장점이 있습니다.

두번째는 코드가독성을 엄청나게 높일 수 있습니다

아래는 실제 수정했던 코드중 일부분 입니다

Post라는 부모 객체에서 자식의 자식…에 접근하기 위하여 prefetch연산을 엄청많이 사용하고 있었습니다. 이 쿼리셋이 여러번 수행되기 때문에 N+1 쿼리로 API 한번 호출에 100건이 넘는 쿼리가 수행되고 있었습니다.

쿼리질의를 inner join으로 하여도 무관하였기 때문에 prefetch_related를 제거하고 아래처럼 filter조건으로 추가하였습니다.

26줄의 코드를 5줄로 줄임으로써 성능을 향상시키고 가독성을 높일 수 있게되었습니다.

Tip 4. Prefetch_related 연산을 저장하여 쿼리 질의 수를 줄이는 방법

  • prefetch 연산 결과를 to_attr 인자로 할당하여 접근할 때 마다 수행되는 쿼리를 제거할 수 있습니다.
queryset = Item.objects.prefetch_related(
Prefetch(
'related_item',
queryset=ItemImage.objects.all(),
to_attr='to_item_image'
)
)

Item쿼리셋 객체에 to_item_image라는 필드가 생성되고, ItemImage 객체의 속성을 접근해도 쿼리가 수행되지 않습니다.

예를 들어 위의 쿼리셋 결과로 ItemImage의 url 목록가져오는 코드는 다음과 같습니다.

위의 쿼리결과는 쿼리를 추가적으로 또 수행해야 합니다. related_itemQuerySetManager이기 때문에 명시적으로 .all()로 쿼리를 수행해야 ItemImage 객체에 접근할 수 있습니다.

아래 결과는 이미 prefetch할때 결과를 list 형태로 담고 있기 때문에 더 이상의 쿼리질의가 일어나지 않습니다.

이게 좋은 부분은, viewset에서 쿼리셋을 수행시 to_attr에 담아두고, serializer에서 가져다 쓸때 쿼리가 수행을 획기적으로 줄일 수 있게 됩니다.

to_attr 만 잘써도 성능개선을 할 수 있습니다 :)

이상. 쿼리셋에 대한 내용을 정리했습니다.

네줄로 요약하자면

  1. prefetch_related 연산은 쿼리가 여러번 수행된다
  2. ORM 필터 조건에 따라서 select_related 보다 prefetch_related가 유리할 때도 있다

3. relation(prefetch_related, select_related)를 지정하지 않고 filter에 조건을 추가하면 자동으로 inner join 질의가 수행된다

4. prefetch_related 연산결과를 to_attr 인수에 담아서 사용하자

참고

--

--