Django ORM

select_related в Django ORM | Курс Django ORM урок 4.1

select_related в Django ORM | Курс Django ORM урок 4.1
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 2

Цель урока

Разобрать select_related, механизм загрузки связанных объектов через SQL JOIN. Понять когда он решает проблему лишних запросов, как контролировать глубину выборки и когда его применение избыточно.

Необходимые знания

  • Урок 1.2: ForeignKey, OneToOneField
  • Урок 2.2: QuerySet API, ленивость
  • Базовое понимание JOIN в SQL

Проблема без 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.


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

  1. Выполните в shell запрос продуктов без select_related и с ним. Посчитайте количество запросов SQL в каждом случае при переборе 5 продуктов с выводом product.category.name.

  2. Загрузите все OrderItem с продуктом и категорией продукта за один запрос. Какой SQL генерируется? Сколько JOIN?

  3. Загрузите заказы вместе с пользователями. Используйте select_related("user"). Посмотрите, какой JOIN используется (INNER или LEFT OUTER) и почему?

  4. Добавьте 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.


<< Урок 3.6

Урок 4.2 >>


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

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

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

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

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

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

Пишите info@aisferaic.ru

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