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는 쿼리셋을 반환할 때 foreign-key, OneToOne관계인 모델을 가져오는 ORM이다. (SQL의 JOIN을 사용함)
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은 foreign-key, OneToOne뿐만 아니라 ManyToMany, ManyToOne등 모든 관계에서 쓰일 수 있다.
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수를 줄여주는 것이 효과적!