prefetch_related в Django ORM | Курс Django ORM урок 4.2
Цель урока
Разобрать prefetch_related, механизм загрузки связанных объектов через отдельные запросы с объединением в Python. Изучить объект Prefetch для сложных сценариев: фильтрация prefetch, вложенный prefetch, prefetch с аннотациями.
Необходимые знания
Как работает prefetch_related
В отличие от select_related (один запрос с JOIN), prefetch_related делает отдельный запрос для каждого связанного объекта и объединяет результаты в Python:
orders = Order.objects.prefetch_related("items").filter(user_id=1)
SQL, два запроса:
-- Запрос 1: заказы
SELECT * FROM shop_order WHERE user_id = 1 ORDER BY created_at DESC;
-- Запрос 2: все позиции для найденных заказов
SELECT * FROM shop_orderitem WHERE order_id IN (1, 3, 7, 12);
Django получает ids заказов из первого запроса, затем одним запросом IN загружает все связанные объекты. В памяти каждый order.items.all() отдает данные из кеша, без новых запросов.
Итого: всегда 2 запроса вне зависимости от количества заказов (не N+1).
Когда prefetch_related, когда select_related
| Тип связи | Метод |
|---|---|
| ForeignKey (прямая) | select_related |
| OneToOneField (прямая) | select_related |
| ForeignKey (обратная) | prefetch_related |
| ManyToManyField | prefetch_related |
# Прямая FK, select_related (JOIN)
Product.objects.select_related("category")
# Обратная FK, prefetch_related (отдельный запрос)
Order.objects.prefetch_related("items")
# M2M, prefetch_related
Product.objects.prefetch_related("tags")
Несколько prefetch
# Загрузить заказ с позициями и отзывами на продукты из позиций
Order.objects.prefetch_related("items", "items__product__reviews")
SQL: 4 запроса:
-- 1. Заказы
SELECT * FROM shop_order WHERE user_id = 1;
-- 2. Позиции заказов
SELECT * FROM shop_orderitem WHERE order_id IN (1, 3, 7);
-- 3. Продукты позиций
SELECT * FROM shop_product WHERE id IN (2, 5, 8, 11);
-- 4. Отзывы на эти продукты
SELECT * FROM shop_review WHERE product_id IN (2, 5, 8, 11);
Каждый уровень вложенности, отдельный запрос. Количество запросов: 1 + количество уровней prefetch. Не N+1.
Объект Prefetch - расширенный контроль
Строковый аргумент в prefetch_related("items") загружает все объекты без фильтрации. Prefetch объект дает полный контроль:
from django.db.models import Prefetch
# Загрузить только позиции с quantity > 1
Order.objects.prefetch_related(
Prefetch(
"items",
queryset=OrderItem.objects.filter(quantity__gt=1),
)
)
SQL:
-- Запрос 1: заказы
SELECT * FROM shop_order;
-- Запрос 2: только позиции с quantity > 1
SELECT * FROM shop_orderitem WHERE order_id IN (...) AND quantity > 1;
to_attr - сохранить в отдельный атрибут
# Для двух prefetch с разными фильтрами, нужны разные атрибуты
Order.objects.prefetch_related(
Prefetch(
"items",
queryset=OrderItem.objects.filter(quantity__gt=1).select_related("product"),
to_attr="bulk_items", # сохранить как список, не RelatedManager
),
Prefetch(
"items",
queryset=OrderItem.objects.filter(quantity=1),
to_attr="single_items",
),
)
for order in orders:
print(order.bulk_items) # список объектов OrderItem с quantity > 1
print(order.single_items) # список объектов OrderItem с quantity = 1
С to_attr результат сохраняется как список (не RelatedManager). Это означает:
- на нём нельзя вызывать
.filter() - зато быстрее, нет накладных расходов RelatedManager
Prefetch с аннотациями
Внутри Prefetch можно передать queryset с аннотациями, они применятся к каждому связанному объекту:
from django.db.models import Prefetch, Avg
# Загрузить отзывы с пользователями, отсортированные по дате
# и аннотировать продукты средним рейтингом из этих же отзывов
Product.objects.prefetch_related(
Prefetch(
"reviews",
queryset=Review.objects.select_related("user").order_by("-created_at"),
)
).annotate(avg_rating=Avg("reviews__rating"))
-- Запрос 1: продукты с аннотацией среднего рейтинга
SELECT shop_product.*,
AVG(shop_review.rating) AS avg_rating
FROM shop_product
LEFT OUTER JOIN shop_review ON shop_product.id = shop_review.product_id
GROUP BY shop_product.id
ORDER BY shop_product.name;
-- Запрос 2: отзывы с пользователями, отсортированные
SELECT shop_review.*, auth_user.*
FROM shop_review
INNER JOIN auth_user ON shop_review.user_id = auth_user.id
WHERE shop_review.product_id IN (1, 2, 3, ...)
ORDER BY shop_review.created_at DESC;
annotate() считается в основном запросе через JOIN+GROUP BY, Prefetch загружает полные объекты отзывов отдельным запросом. Итого 2 запроса, и у каждого продукта есть и product.avg_rating, и product.reviews.all() из кеша.
Комбинирование select_related и prefetch_related
Можно и нужно использовать оба:
# Заказы → позиции (prefetch) → продукт каждой позиции (select_related внутри prefetch)
Order.objects.prefetch_related(
Prefetch(
"items",
queryset=OrderItem.objects.select_related("product__category"),
)
)
SQL:
-- Запрос 1: заказы
SELECT * FROM shop_order;
-- Запрос 2: позиции + продукты + категории (через JOIN)
SELECT shop_orderitem.*,
shop_product.*,
shop_category.*
FROM shop_orderitem
INNER JOIN shop_product ON shop_orderitem.product_id = shop_product.id
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE shop_orderitem.order_id IN (...);
Два запроса, полная структура Order → OrderItem → Product → Category.
prefetch_related и изменение QuerySet после загрузки
Важное ограничение: если после загрузки вызвать .filter() на атрибуте prefetch, кеш сбрасывается и выполняется новый запрос:
orders = Order.objects.prefetch_related("items")
for order in orders:
# Использует кеш, нет запроса
all_items = order.items.all()
# Сбрасывает кеш, новый запрос!
expensive_items = order.items.filter(price__gt=500)
Если нужна фильтрация, используйте Prefetch с to_attr и фильтруй список в Python:
orders = Order.objects.prefetch_related(
Prefetch("items", queryset=OrderItem.objects.all(), to_attr="all_items")
)
for order in orders:
expensive_items = [i for i in order.all_items if i.price > 500] # Python filter
prefetch_related_objects() - prefetch для уже загруженных объектов
Если объекты уже загружены, можно добавить prefetch постфактум:
from django.db.models import prefetch_related_objects
orders = list(Order.objects.filter(user_id=1))
# Добавить prefetch к уже загруженным объектам
prefetch_related_objects(orders, "items__product")
Полезно когда объекты приходят из кеша или из другого места, а prefetch нужно добавить без нового запроса к основной таблице.
Практическое задание
-
Загрузите все заказы пользователя с id=1 вместе с позициями. Посчитайте количество запросов SQL с
prefetch_relatedи без него. -
Загрузите продукты с тегами и отзывами. Используйте
prefetch_related("tags", "reviews"). Сколько запросов? -
Загрузите заказы с позициями, но только те позиции где
quantity >= 2. ИспользуйтеPrefetchс queryset. Посмотрите SQL второго запроса. -
Реализуйте загрузку заказов с двумя типами позиций:
bulk_items(quantity > 1) иsingle_items(quantity = 1) черезPrefetchсto_attr. -
Загрузите продукты с отзывами через
prefetch_related. Попробуйтеproduct.reviews.filter(rating=5), посмотрите в логе SQL что происходит (новый запрос?). Затем замените наPrefetch(..., to_attr="all_reviews")и фильтрацию в Python.
Возможные ошибки
filter() на атрибуте prefetch инвалидирует кеш
Любой вызов .filter(), .exclude(), .order_by() и других методов на атрибуте prefetch создает новый QuerySet, Django не знает про кеш prefetch, возникает N+1:
# N новых запросов SQL, prefetch не помогает
for product in Product.objects.prefetch_related("reviews"):
good = product.reviews.filter(rating__gte=4) # кеш проигнорирован!
# SQL: SELECT * FROM shop_review WHERE product_id = X AND rating >= 4
Правило: единственный безопасный вызов на атрибуте prefetch: .all(). Всё остальное, новый запрос.
Решение: перенести фильтрацию в Prefetch. А финальную фильтрацию делать в Python:
# Правильно, фильтр внутри Prefetch, результат в кеше
for product in Product.objects.prefetch_related(
Prefetch("reviews", queryset=Review.objects.filter(rating__gte=4), to_attr="good_reviews")
):
good = product.good_reviews # список из кеша, без новых запросов
prefetch_related для прямых FK
# Работает, но неэффективно, делает запрос IN вместо JOIN
Product.objects.prefetch_related("category")
# Правильно для прямых FK
Product.objects.select_related("category")
Забыть select_related внутри Prefetch
# Для каждой позиции будет отдельный запрос к shop_product
Order.objects.prefetch_related("items")
for order in orders:
for item in order.items.all():
print(item.product.name) # N запросов!
# Правильно
Order.objects.prefetch_related(
Prefetch("items", queryset=OrderItem.objects.select_related("product"))
)
Связь со следующим уроком
В уроке 4.3 диагностируем N+1 проблему с помощью Django Debug Toolbar, смотрим реальные цифры, находим где теряется производительность и измеряем улучшение после применения select_related и prefetch_related.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru