select_related в Django ORM | Курс Django ORM урок 4.1
Цель урока
Разобрать select_related, механизм загрузки связанных объектов через SQL JOIN. Понять когда он решает проблему лишних запросов, как контролировать глубину выборки и когда его применение избыточно.
Необходимые знания
Проблема без select_related
Рассмотрим простой вывод списка продуктов с названием их категории:
products = Product.objects.filter(is_active=True)
for product in products:
print(f"{product.name} - {product.category.name}")
SQL который выполняется:
-- Запрос 1: загрузка продуктов
SELECT * FROM shop_product WHERE is_active = true ORDER BY name;
-- Запрос 2: категория для продукта 1
SELECT * FROM shop_category WHERE id = 3;
-- Запрос 3: категория для продукта 2
SELECT * FROM shop_category WHERE id = 3; -- та же категория, но новый запрос
-- Запрос 4: категория для продукта 3
SELECT * FROM shop_category WHERE id = 1;
-- ... N дополнительных запросов для N продуктов
При 15 продуктах: 16 запросов. Это N+1 проблема. Подробно разберем её в уроке 4.3, сейчас важно понять механизм решения.
select_related - загрузка через JOIN
products = Product.objects.select_related("category").filter(is_active=True)
for product in products:
print(f"{product.name} - {product.category.name}") # нет лишних запросов
SELECT shop_product.*,
shop_category.id AS category_id,
shop_category.name AS category_name,
shop_category.slug AS category_slug,
shop_category.parent_id AS category_parent_id
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE shop_product.is_active = true
ORDER BY shop_product.name;
Один запрос вместо N+1. Django загружает продукты и их категории через JOIN, складывает данные категории в кешированный объект product._category_cache. При обращении к product.category, данные берутся из кеша, SQL не выполняется.
Несколько связей
# Загрузить продукт, его категорию и родительскую категорию
Product.objects.select_related("category", "category__parent")
SELECT shop_product.*,
cat.id, cat.name, cat.slug, cat.parent_id,
parent_cat.id, parent_cat.name, parent_cat.slug, parent_cat.parent_id
FROM shop_product
INNER JOIN shop_category AS cat ON shop_product.category_id = cat.id
LEFT OUTER JOIN shop_category AS parent_cat ON cat.parent_id = parent_cat.id
ORDER BY shop_product.name;
LEFT OUTER JOIN для category__parent, потому что parent может быть NULL (корневая категория).
Глубина выборки
select_related() без аргументов рекурсивно загружает все FK и OneToOne связи:
# Загрузит все связанные объекты по всем FK
Product.objects.select_related()
Это удобно, но опасно, может затянуть лишние таблицы. Лучше явно указывать что нужно:
# Только категория
Product.objects.select_related("category")
# Категория и её родитель
Product.objects.select_related("category__parent")
select_related и OneToOneField
select_related работает как для FK, так и для OneToOne, в обе стороны:
# Прямая связь: загрузить профиль при запросе пользователей
User.objects.select_related("profile")
# Обратная связь: загрузить пользователя при запросе профилей
UserProfile.objects.select_related("user")
-- User + Profile
SELECT auth_user.*, shop_userprofile.*
FROM auth_user
LEFT OUTER JOIN shop_userprofile ON auth_user.id = shop_userprofile.user_id;
Когда select_related НЕ подходит
select_related работает только с FK и OneToOneField: то есть с отношениями где на стороне запроса есть одно значение (category_id). Для ManyToMany и обратных FK (один ко многим), нужен prefetch_related (урок 4.2).
# select_related не поможет для обратной FK
Order.objects.select_related("items") # FieldError: обратные FK не поддерживаются
# items - это обратная связь (один заказ → много позиций)
# Правильно для обратных FK и M2M
Order.objects.prefetch_related("items")
Влияние на производительность
Когда select_related ускоряет
- Нужны данные связанного объекта для каждой строки
- Связанный объект один (FK, OneToOne)
- JOIN не умножает строки, нет риска декартова произведения
Когда select_related избыточен
# Не нужен category если обращаетесь только к category_id
for product in Product.objects.select_related("category"):
print(product.category_id) # category_id уже есть в product, JOIN лишний
# Не нужен если данные категории вообще не используются
for product in Product.objects.select_related("category"):
print(product.name) # category не нужна - select_related добавляет лишний JOIN
SELECT * vs только нужные поля
select_related загружает все поля связанной модели. Если модель тяжелая (много полей или TextField), используйте only() для ограничения:
Product.objects.select_related("category").only(
"name", "price",
"category__name", "category__slug",
)
SELECT shop_product.id, shop_product.name, shop_product.price,
shop_category.id, shop_category.name, shop_category.slug
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
ORDER BY shop_product.name;
Ловушка: only() без полей связанной модели вызывает N+1
При использовании only() с select_related нужно явно включить поля связанной модели через category__field. Если не включить, при первом обращении к любому полю категории Django выполнит отдельный SELECT:
# N+1! category__name не включен в only()
products = Product.objects.select_related("category").only("name", "price")
for p in products:
print(p.category.name) # SELECT shop_category WHERE id=X - для каждого продукта
# Правильно - включить нужные поля связанной модели
products = Product.objects.select_related("category").only(
"name", "price",
"category__name", # обязательно!
)
for p in products:
print(p.category.name) # данные уже в JOIN, нет доп. запросов
Правило: при комбинации select_related + only() всегда явно перечисляй нужные поля каждой связанной модели с префиксом relation__field.
Практическое задание
-
Выполните в
shellзапрос продуктов безselect_relatedи с ним. Посчитайте количество запросов SQL в каждом случае при переборе 5 продуктов с выводомproduct.category.name. -
Загрузите все
OrderItemс продуктом и категорией продукта за один запрос. Какой SQL генерируется? Сколько JOIN? -
Загрузите заказы вместе с пользователями. Используйте
select_related("user"). Посмотрите, какой JOIN используется (INNER или LEFT OUTER) и почему? -
Добавьте
only()к запросу из задания 2, загружай толькоquantity,priceиз OrderItem,nameиз Product иnameиз Category. Сравните SQL до и после.
Возможные ошибки
select_related для обратных FK и M2M
# Нет эффекта или ошибка
Order.objects.select_related("items") # обратная FK
Product.objects.select_related("tags") # M2M
# Правильно
Order.objects.prefetch_related("items")
Product.objects.prefetch_related("tags")
select_related() без аргументов на модели с многими связями
# Загружает все FK рекурсивно, может добавить десятки JOIN
Order.objects.select_related()
# Лучше явно
Order.objects.select_related("user")
Лишний select_related когда нужен только _id
# JOIN ради поля, которое и так есть
products = Product.objects.select_related("category")
for p in products:
print(p.category_id) # category_id есть без JOIN
# Правильно, без select_related
products = Product.objects.all()
for p in products:
print(p.category_id) # читается напрямую из колонки
Связь со следующим уроком
В уроке 4.2 разберем prefetch_related, механизм для M2M и обратных FK. В отличие от select_related он делает отдельные запросы и объединяет результаты в Python. Разберем Prefetch объект для сложных сценариев: фильтрация prefetch, вложенный prefetch.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru