select_related와 prefetch_related

jm_yoon·2021년 3월 8일
0

Django ORM 사용할 때 Query 개수를 줄일 수 있는 방법에는 select_related와 prefetch_related가 있다.

select_related 와 prefetch_related 는 하나의 QuerySet을 가져올 때, 미리 related objects들까지 다 불러와주는 함수이다.
그렇게 불러온 data들은 모두 cache에 남아있게 되므로 DB에 다시 접근해야 하는 수고를 덜어줄 수 있다. 이렇게 두 함수 모두 DB에 접근하는 수를 줄여, performance를 향상시켜준다는 측면에서는 공통점이 있지만, 그 방식에는 차이점이 있다.

예시모델

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=255)

    class Meta:
        db_table = 'publishers'

    def __str__(self):
        return self.name


class Book(models.Model):
    name = models.CharField(max_length=255)
    price = models.IntegerField(default=0)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

    class Meta:
        db_table = 'books'

    def __str__(self):
        return self.name


class Store(models.Model):
    name = models.CharField(max_length=255)
    books = models.ManyToManyField(Book)

    class Meta:
        db_table = 'stores'

    def __str__(self):
        return self.name

select_related

select_related는 쿼리셋을 반환할 때 foreign-key, OneToOne관계인 모델을 가져오는 ORM이다. (SQL의 JOIN을 사용함)

정참조(OneToMany관계)에서 DB hit 줄이는 방법

views.py(select_related 적용 전)

class BooksWithAllMethodView(View):
    def get(self, request):
        queryset = Book.objects.all()

        books = []
        for book in queryset:  # queryset 평가
            books.append({
                'id': book.id,
                'name': book.name,
                'publisher': book.publisher.name # book.publisher에 접근, 캐싱되지 않은 데이터이므로 query 발생
                }
            )
        return JsonResponse({'books_with_all_method' : books }, status=200)

실행된 쿼리문

SELECT books.id, books.name, books.price, books.publisher_id
FROM books 

SELECT publishers.id, publishers.name
FROM publishers
WHERE publishers.id = 1

SELECT publishers.id, publishers.name
FROM publishers
WHERE publishers.id = 1

SELECT publishers.id, publishers.name
FROM publishers
WHERE publishers.id = 1

...

Book 모델에서 data를 가져오는 쿼리 1개
Book모델에 걸려있는 publishers모델을 가져오는 쿼리(book의 개수만큼 쿼리 발생)


views.py(select_related 적용 후)

class BooksWithSelectRelatedView(View):
    def get(self, request):
        queryset = Book.objects.select_related("publisher").all()

        books = []
        for book in queryset:  # queryset 평가
            books.append({
                'id': book.id,
                'name': book.name,
                'publisher': book.publisher.name
                }
            )
        return JsonResponse({'books_with_all_method' : books }, status=200)

실행된 쿼리문

SELECT books.id, books.name, books.price, books.publisher_id, publishers.id, publishers.name 
FROM books 
INNER JOIN publishers 
ON books.publisher_id = publishers.id

select_related는 INNER JOIN으로 data를 가져오기 때문에 쿼리문 하나로 해결가능!

prefetch_related

prefetch_related은 foreign-key, OneToOne뿐만 아니라 ManyToMany, ManyToOne등 모든 관계에서 쓰일 수 있다.

역참조(Many-To-Many 관계) 상황에서 DB hit 줄이는 방법

views.py(prefetch_related 적용 전)

class StoresWithAllMethodView(View):
    def get(self, request):
        queryset = Store.objects.all()

        stores = []
        for store in queryset:  # 쿼리셋 평가
            books = [book.name for book in store.books.all()]  # 각 store마다 books로 접근, 쿼리 발생
            stores.append({
                'id': store.id,
                'name': store.name,
                'books': books
                }
            )
        return JsonResponse({'stores_with_all_method' : stores }, status=200)

실행된 쿼리문

SELECT stores.id, stores.name
FROM stores;

SELECT books.id, books.name, books.price, books.publisher_id
FROM books 
INNER JOIN stores_books 
ON books.id = stores_books.book_id 
WHERE stores_books.store_id = 1

SELECT books.id, books.name, books.price, books.publisher_id
FROM books 
INNER JOIN stores_books 
ON books.id = stores_books.book_id 
WHERE stores_books.store_id = 1

SELECT books.id, books.name, books.price, books.publisher_id
FROM books 
INNER JOIN stores_books 
ON books.id = stores_books.book_id 
WHERE stores_books.store_id = 1

SELECT books.id, books.name, books.price, books.publisher_id
FROM books 
INNER JOIN stores_books 
ON books.id = stores_books.book_id 
WHERE stores_books.store_id = 1

...

views.py(prefetch_related 적용 후)

class StoresWithPrefetchRelatedView(View):
    def get(self, request):
        queryset = Store.objects.prefetch_related("books").all()

        stores = []
        for store in queryset:
            books = [book.name for book in store.books.all()]
            stores.append({'id': store.id, 'name': store.name, 'books': books})

        return JsonResponse({'stores_with_prefetch_related' : stores }, status=200)

실행된 쿼리문

SELECT stores.id, stores.name
FROM stores;

SELECT stores_books.store_id, books.id, books.name, books.publisher_id 
FROM books 
INNER JOIN 
stores_books 
ON books.id = stores_books.book_id 
WHERE stores_books.store_id IN (1, 2, 3, 4, 10, 50, ....) ;

Store.objects.all() 이라는 쿼리가 실행되면서 동시에 store에 걸려있는 book의 data들이 cache에 저장된다.
store의 수 만큼 book이 실행되더라도 DB에 접근하지 않고 cache에서 찾아서 쓰게 된다.

정리

prefetch_related은 원래의 main query가 실행된 후 별도의 query를 따로 실행하고 select_related은 하나의 query만으로 related objects들을 다 가져온다.

MTM MTO의 관계에서는 prefetch_related를 사용해야 하지만 foreign_key, OTO과 같은 single-valued 관계가 있는 곳에서는 최대한 select_related를 사용하여 query수를 줄여주는 것이 효과적!

Caching and QuerySets
참고
참고

profile
Hello!

0개의 댓글