Оптимизация запросов в Django ORM
Сегодня хочу рассказать про методы select_related и prefetch_related: показать разницу между ними, привести примеры использования.
Кратко
Если говорить совсем кратко, то: select_related — это JOIN, то есть происходит 1 запрос, в рамках которого связываются указанные сущности и приходят в python уже связанными, в то время, как prefetch_related — делает отдельные запросы по каждой указанной связанной сущности и производит связывание на уровне python.
В документации django сказано, что select_related стоит применять в тех случаях, когда вы используете ForeignKey, если вы используете ManyToManyField, ваш выбор — это prefetch_related.
Разбираем на примерах
Предположим, что наши модели выглядят следующим образом:
from django.db import models
class Genre(models.Model):
name = models.CharField(max_length=512)
class City(models.Model):
...
class Person(models.Model):
hometown = models.ForeignKey(
City,
)
class Book(models.Model):
name = models.CharField(max_length=512)
author = models.ForeignKey(Person, on_delete=models.CASCADE)
genres = models.ManyToManyField(Genre)Перед нами поставлена задача: получить информацию о книге, её авторе и его родном городе.
Код для решения этой задачи с select_related и без:
# Отправим запрос в БД, чтобы присоединить (выполнить JOIN) связанные записи по таблицам author и hometown.
book = Book.objects.select_related("author__hometown").get(id=4)
author = book.author # Дополнительный запрос не будет отправлен.
city = author.hometown # Дополнительный запрос не будет отправлен.
# Без использования select_related()...
book = Book.objects.get(id=4) # Запрос будет отправлен.
author = book.author # Запрос будет отправлен.
city = author.hometown # Запрос будет отправлен.Таким образом, при использовании select_related, мы можем отправить один запрос вместо трёх.
⚠️ В случае, если вы получаете объект Queryset, к которому уже были применены select_related и вы хотите от них избавиться, вам следует использовать select_related(None).
Кроме этой информации, у каждой книги может быть несколько жанров, следующая задача, которую нам предстоит решить: реализовать вывод названия книги и перечисление всех её жанров в строковом описании объекта модели. Реализация довольно простая и никаких проблем не вызывает:
from django.db import models
class Genre(models.Model):
name = models.CharField(max_length=512)
class Book(models.Model):
name = models.CharField(max_length=512)
genres = models.ManyToManyField(Genre)
def __str__(self):
return f"{self.name} ({', '.join(genre.name for genre in self.genres.all())})"Где же тут может понадобиться prefetch_related? Всё очень просто. Чаще всего, строковое представление помогает визуально отобразить в админ-панели понятный список объектов, в таком случае будет выполняться следующий запрос:
Book.objects.all()
Результатом выполнения такого запроса будет генерация дополнительного запроса к БД для получения списка жанров по каждой книге.
Исправить это можно, как раз, с помощью prefetch_related:
Book.objects.prefetch_related("genres")В этом случае мы отправим всего 2 запроса:
⚠️ В случае, если вы получаете объект Queryset, к которому уже были применены prefetch_related и вы хотите от них избавиться, вам следует использовать prefetch_related(None).
Подробности и подводные камни
Инвалидация кэша
⚠️ При использовании prefetch_related, важно помнить о такой вещи, как инвалидация кэша на уровне Queryset (инвалидация может быть вызвана любым дополнительным методом в цепочке, который подразумевает другой запрос к базе данных). Например, при использовании фильтрации в следующем примере, будут выполнены отдельные запросы для каждого объекта Book:
for book in Book.objects.prefetch_related("genres"):
print(book.name, ":")
for genre in Book.genres.filter(age_rating="18+"):
print(" ", genre.name)Объект Prefetch
Также, стоит упомянуть и о существовании объекта Prefetch, который позволяет использовать подготовленный Queryset вместе с prefetch_related. Например, мы хотим отобрать заранее только те жанры, которые имеют возрастной рейтинг "18+" и посчитать количество книг по каждому такому жанру.
Код для решения этой задачи выглядит следующим образом:
genres = Genre.objects.filter(age_rating="18+")
.annotate(books_count=Count("book_set"))
queryset = Book.objects.all().prefetch_related(
Prefetch("genres", queryset=genres)
)Функция prefetch_related_objects
В случае, если вы получаете не объект Queryset, а коллекцию экземпляров моделей, вы всё ещё можете использовать prefetch_related посредством обращения к функции prefetch_related_objects, например:
from django.db.models import prefetch_related_objects books = fetch_top_rated_books_from_cache() prefetch_related_objects(books, "genres")
Источники
- Статья с хабра, в которой всё доходчиво объясняют: https://habr.com/ru/articles/752574/
- Документация Django: https://docs.djangoproject.com/en/5.0/ref/models/querysets/