Оптимизация запросов в 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/