본문 바로가기
Web/Django

[Django] DB 쿼리 최적화를 다양한 방법들을 정리해보았다.

by DUSTIN KANG 2023. 12. 20.

지난 쿼리셋 포스팅에 이어서 최적화를 하는 다양한 방법에 대해 적어보려한다. 

 

데이터 성능을 개선시키는 메서드들

`only()`메서드를 활용하자.

기본적으로 우리가 어떠한 데이터들을 가져온다고 가정했을 때 `Track.objects.all()`와 같은 식으로 쿼리한다. 해당 쿼리는 track 데이터의 모든 필드를 가져오는 명령어로 만약 우리가 전체 컬럼이 아닌 특정한 필드를 가져오고 싶다면 위와 같이 사용하지 않아도 된다.

`only()`메서드를 이용하면 지정된 필드만 가져올 수 있게 설정할 수 있다. 만약 같은 `title`이란 필드만 조회한다고 가정하면 쿼리에 사용할 수 있는 메모리를 줄여 DB 부하를 줄여줄 수 있지 않을까 한다.

 

 

하지만 장점만 가지고 있는 것은 아니다.

만약 다음 코드와 같이 지정해두지 않는 필드(`duration`)을 불러온 경우에는 쿼리가 두번 실행되는 점이다. 즉, 불필요한 쿼리가 실행된다.

def example(request):
    track = Track.objects.only('title').get(id='4SFknyjLcyTLJFPKD2m96o')
    print(track.duration)
    return HttpResponse(status=200)

우측이 only()메서드를 추가한 후의 결과이다.

만약 제외할 필드를 선택한다고 하면 `defer()`메서드를 사용하면 된다.

 

`values()`메서드를 활용하자.

이것도 전송량과 메모리를 덜 사용하는 방법으로 딕셔너리, 튜플 , 리스트 형태의 데이터 구조로 만들어 불필요한 오버헤드를 줄일 수 있다.

뿐만 아니라 해당하는 필드 값을 넣어 필요한 필드 값만 반환할 수 있다.

  • values() : 쿼리셋의 값을 딕셔너리로 반환한다.
  • values_list() : 쿼리셋의 값을 튜플 형태로 반환한다.
  • values_list(flat=True) : 쿼리셋의 값을 리스트로 반환한다.

Eager Loading

Eager Loading은 이전에 배웠던 Lazy Loading과 차이점이 있다.

Lazy Loding은 필요한 시점에 쿼리를 날리고 이전까지는 DB에서 데이터를 가져오지 않는 것을 의미한다. 이유는 Query 구현체 내부에 `result_cache`라는 변수가 있는데 순서에 맞게 불러온 데이터들을 캐싱하여 SQL 쿼리를 발생하기 때문이다. 

즉, 출력이나 반복문을 돌리기 전에 캐싱을 쌓아둔다는 것이다. 이렇게 되면 중복되는 쿼리가 발생하게 될수도 있다는 비효율적인 문제가 있다. 뿐만아니라 Lazy-Loding은 게을러 빠졌기 때문에 참조되는 데이터를 가져오지 않고 해당 모델에 있는 필드만 가져오게 되므로 쿼리가 쌓일 수 밖에 없다. 이 문제를 우리는 N + 1 문제라고 한다.

 

 

이러한 문제를 해결하는 법이 Eeger-Loading(즉시 로딩)이다.

Eager-Loading은 Lazy-Loading과 다르게 필요한 정보를 미리 로딩하는 방식이다. 추가 쿼리 없이 데이터를 미리 가져와 캐싱하여 빠르게 접근할 수 있다는 장점을 가지고 있다. Django에서는 미리 연관이 있는 객체를 가져오기 위해 `select_related`와 `prefetch_related`가 있다. 이 두가지 메소드를 통해 DB에 접근(Connection)하는 횟수를 줄여주기 때문에 성능을 개선시킬 수 있다.

select_related

먼저 select_related는 1:N 관계에서 N+1 문제를 해결할 수 있는 방법이다.

상위 이미지에서 album이라는 객체에 OnetoOneField은 artist라는 필드를 가져오려고 했다. 그러나 이상태에서는 Album 테이블을 조회하고 추가로 Artist 테이블을 조회하는 식으로 N+1 문제가 발생하게 된다.

이때, Select_related를 사용하면 `JOIN`을 함으로 써 쿼리를 1번만 진행하면 되기 때문에 문제를 해결할 수 있게 된다. 

 

select_related()는 정참조를 통해 사용할 수 있다.

또한 여러개의 필드를 지정할 수도 있다. 여기서 추가로 알아둬야할 점은 album의 artist 필드가 `on_delete=models.CASCADE`로 옵션을 설정했기 때문에 INNER JOIN이 실행됬다. 만약 `SET_NULL`로 설정했다면 `LEFT OUTER JOIN`이 실행되었을 것이다.

공식 문서 참조

prefetch_related

이번엔 M:N 관계 또는 외래키의 역참조 관계에서 발생할 수 있는 N+1 문제를 해결할 수 있는 방법이다.

예를 들어, 다음과 같이 코드를 작성하면 N+1의 문제가 발생한다. Album과 Track의 역참조 관계를 사용하여 트랙의 제목을 불러오는 뷰 함수이다. 이러한  비효율의 쿼리들을 줄이기 위해 `prefetch_related`를 사용하게 된다.

@silk_profile(name="Not prefetch_related")
def example(request):
    albums = Album.objects.all()
    for album in albums:
        context = (album.title, [track.title for track in album.tracks.all()])
    return HttpResponse(context)

 

여기서, select_related와 prefetch_related의 차이점이라고 본다면, Select_related는 JOIN을 실행하지만 Prefetch_related는 파이썬 단계에서 JOIN을 만들어 내기 때문에 필터링을 사용한다는 것이다. 

@silk_profile(name="prefetch_related")
def example(request):
    albums = Album.objects.all().prefetch_related('tracks')
    context = []
    for album in albums:
        context.append([track.title for track in album.tracks.all()])
    return HttpResponse(context)

 

위 그림을 보면 Select 쿼리를 두번 수행하게 된다. Album 정보 쿼리를 수행하고 두번째에는 역참조 필드를 사용하는 추가 쿼리이다. 그렇다면 우리가 역 참조 필드 값을 tracks만 설정했는데 이 필드가 N개라면 N의 추가 쿼리가 생성될 것이다. (지금은 1개의 필드만 늘어난 것이다.)

결론적으로, Select_related와 차이라면 추가 쿼리가 발생하고 SQL JOIN이 아닌 파이썬 단계에서만 JOIN이 된다는 점이다. 파이썬 단계에서 Join한다는 말은 다른 쿼리를 실행하여 중복 열을 줄이게 한다는 말이다. 참고

 

하지만 주의할 점이 있다.

외래키에 대해 `prefetch_related`를 사용한 후 필터링을 하게되면 추가 쿼리가 발생한다는 점이었다.

@silk_profile(name="prefetch_related")
def example(request):
    albums = Album.objects.all().prefetch_related('tracks')
    context = []
    for album in albums:
        tracks = album.tracks.filter(title__contains='Love')
        for track in tracks:
            context.append(track)
    return HttpResponse(context)

 

 

이러한 경우는 어떻게 하면 쿼리 수를 줄일 수 있을까?

`Prefetch`라는 객체를 사용하면 쿼리 수를 줄일 수 있다.  Prefetch는 필터링, 정렬등 필터 조건을 사용해 데이터를 디테일하게 제어할 수 있다. 

from django.db.models import Prefetch
from .models import Track, Album
from silk.profiling.profiler import silk_profile

@silk_profile(name="prefetch")
def example(request):
    # Love라는 단어를 필터링하는 Prefetch 객체를 생성한다.
    love_track_prefetch = Prefetch(
        'tracks', # Album 모델의 역참조 필드 이름
        queryset= Track.objects.filter(title__contains='Love') # 필터링을 적용할 쿼리셋
    )
    # Album 객체와 연관된 Track 객체 중 Love를 포함한 노래만 가져오기
    albums = Album.objects.prefetch_related(love_track_prefetch)
    context = []
    for album in albums:
        tracks = album.tracks.all()
        for track in tracks:
            context.append(track.title)
    return HttpResponse(context)

 

위 코드와 같이 Prefetch 객체에 역참조 필드 이름과 쿼리셋을 적용하면 다음과 같이 쿼리 수를 줄일 수 있다.

 

공식 문서 참조

FilteredRelation

FilteredRelation은 특정 조건에 만족하는 관련 객체들을 필터링하는 것을 말한다.

`annotate`를 사용하여 특정 조건을 추가할 수 있다. 

@silk_profile(name="filtered_relation")
def example(request):
    albums = Album.objects.annotate(
        collaboration_tracks = FilteredRelation(
            'tracks',
            condition=Q(tracks__title__contains='(with.'))
         ).annotate(
             collaboration_tracks_count = Count('collaboration_tracks')
         )

    for album in albums:
        context = f"{album.title} has {album.collaboration_tracks_count} songs"
    return HttpResponse(context)

 

마치며

지금까지, 쿼리를 최적화하는 여러가지 방법들에 대해 정리를 해보고 실습을 진행했다. 쿼리셋을 어떻게 작성하느냐에 따라 동작이 달라지는 것을 깨닫게 되었고 실제로 SQL이 어떻게 동작하는가 확인 후 점차 성능을 개선해 나가야겠다고 생각한다. 가정 먼저 기본적으로 모델을 정의 할 때, 정렬된 순서나 unique 설정을 해두면 초기 성능도 좋아지지 않을까 싶다.

 

추후 내가 공부해야할 것

도움이 되는 참고 자료

https://dev-jacob.tistory.com/entry/Django-selectrelated를-알아보자

 

Django select_related를 알아보자

Select_related() 이 전에 잠깐 공부한 적이 있는 select_related인데 깊게 공부하지 못 했던 것 같아 장고 공식문서를 보며 한 번 다시 공부해보려 한다. Select_related()란? select_related()는 쿼리가 실행될 때,

dev-jacob.tistory.com

https://cocook.tistory.com/52

 

[Django] 장고 쿼리셋 파헤치기(Eager Loading)

아래 내용은 김성렬님의 2020 Pycon-Korea Django ORM (QuerySet)구조와 원리 그리고 최적화전략을 정리해둔 내용입니다. https://www.youtube.com/watch?v=EZgLfDrUlrk 장고는 ORM(Object Relational Mapping)을 이용해 데이터

cocook.tistory.com

https://bio-info.tistory.com/174

 

[Django] 간단하게 알아보는 N+1 문제 해결! (select_related, prefetch_related)

Contents 1. 배경 장고에서 데이터를 불러오는 방식은 ORM의 Lazy-Loading을 기본으로 하고 있습니다. 이는 데이터가 필요한 시점에, 알맞은 쿼리를 최적화해서 DB에 날리는 방식입니다. 일반적으론 이

bio-info.tistory.com

https://techblog.yogiyo.co.kr/django-queryset-1-14b0cc715eb7

 

Django에서는 QuerySet이 당신을 만듭니다

ORM with Django — 김성렬, Backend Developer

techblog.yogiyo.co.kr

 

'Web > Django' 카테고리의 다른 글

[Django] 커스텀 Admin 페이지  (0) 2024.01.10
Django Rest Framework - (1 : API 테스트 해보기)  (0) 2023.12.25
[Django] 쿼리셋으로 데이터 조회하기  (0) 2023.12.20
Django - 객체 참조 관계  (0) 2023.11.29
[Django] CRUD 구현  (0) 2023.11.29