본문 바로가기

Python/Django

[Django ORM] QuerySet 수정을 통해 성능 향상시키기 / SQL Queries 중복 줄이기 / select_related, prefetch_related

들어가며

처음 video list 페이지를 들어갈 때마다, video의 개수가 늘어나면서 로딩시간이 눈이띄게 길어진 것을 느낄 수 있었다. 그래서 찾게된 QuerySet 효율적으로 사용하기!

초보몽키의 개발공부로그를 통해 웹 성능을 향상시킬 수 있었다.

기존의 문제점

기존의 video list view를 보면 다음과 같은 방법으로 모든 video를 불러오고 이를 template로 넘긴다.

video_list = Video.objects.all() # 모든 video 불러오기
return render(request, 'video/video_list.html', {'video_list':video_list}) # template에 넘기기

 

그리고 django-debug-toolbar을 통해 queries를 확인해보면, 15 queries와 13 similar, 그리고 7개의 중복이 있는 것을 확인할 수 있다.


select_related와 prefetch_related

select_related

- ForeignKey, OneToOneField와 같은 Single-valued relationships에만 사용

- SQL JOIN을 사용하여 모든 field를 SELECT문으로 포함시킴으로써 related objects를 같은 DB 쿼리로 한번에 가져온다.

 

prefetch_related

- ManyToMany, OneToMany을 포함한 모든 relationships에 사용 가능

- 각각의 relationship에 대해 별도의 쿼리를 날리고 Python으로 'joining'을 한다.

 

그럼 이제 Model과 Template을 살펴보자.

Model

class Video(models.Model):

    class Category(models.TextChoices):
        Music = 'music'
        Movie = 'movie'
        Drama = 'drama'
        Comedy = 'comedy'
        Information = 'info'
        Daily = 'daily'
        Beauty = 'beauty'
        Art = 'art'
        Book = 'book'
        Sport = 'sport'
        Food = 'food'

    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    video_key = models.CharField(max_length=12)
    likes_user = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='likes_user')
    upload_date = models.DateTimeField(auto_now_add=True, null=True) # first created date
    category = models.TextField(choices=Category.choices, blank=True)

Template

<div class="row">
        {% for video in video_list %}
        <div id="video_list_search" class="col-md-5" style="float: left; margin-bottom: 30px; margin-right: 30px;">
            <a href="{% url 'video_detail' pk=video.id %}">
                <img src="http://img.youtube.com/vi/{{video.video_key}}/mqdefault.jpg" alt="thumbnail">
                <h4>
                    {{ video.title }}
            </a>
            <span class="glyphicon glyphicon-heart" style="color:red"></span>{{ video.count_likes_user }}
            </h4>
            <h5>{{ video.upload_date }}&nbsp;&nbsp;by.{{ video.author }}</h5>
        </div>
        {% endfor %}
</div>

QuerySet 수정하기

문제1: auth_user 중복

auth_user이 반복되는 것은, auth_user을 담는 field와 관련하여 쿼리를 수정해야겠다고 생각했다.

그래서 우선 author field를 선택하였고, 이는 ForeignKey이기에

쿼리셋을 다음과 같이 수정하였다.

video_list = Video.objects.select_related('author').all()

 

그랬더니, (15 queries)->9개로, (13 similar, 7개의 중복)->6 similar로 바뀌었다!

문제2: likes_user INNER JOIN similar

 

likes user은 ManyToManyField이기에, prefetch_related를 사용하여

최종적으로 다음과 같이 수정하였다.

video_list = Video.objects.prefetch_related('likes_user').select_related('author').all()

 

그랬더니, 총 4개의 쿼리로 마무리할 수 있었다!


마무리하며

DB 쿼리를 어떻게 쓰느냐에 따라 이렇게 성능에 영향을 많이 받는다는 것을 직접 실감하게 되었다.

앞으로 DB 설계에서 쿼리공부도 많은 노력을 쏟아야 나중에 덜힘들다는 사실을 알았고,

debug tool을 잘 사용해야겠다는 생각을 하였다!