Django ORM

prefetch_related в Django ORM | Курс Django ORM урок 4.2

prefetch_related в Django ORM | Курс Django ORM урок 4.2
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 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 нужно добавить без нового запроса к основной таблице.


Практическое задание

  1. Загрузите все заказы пользователя с id=1 вместе с позициями. Посчитайте количество запросов SQL с prefetch_related и без него.

  2. Загрузите продукты с тегами и отзывами. Используйте prefetch_related("tags", "reviews"). Сколько запросов?

  3. Загрузите заказы с позициями, но только те позиции где quantity >= 2. Используйте Prefetch с queryset. Посмотрите SQL второго запроса.

  4. Реализуйте загрузку заказов с двумя типами позиций: bulk_items (quantity > 1) и single_items (quantity = 1) через Prefetch с to_attr.

  5. Загрузите продукты с отзывами через 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.


<< Урок 4.1

Урок 4.3 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы