Как оптимизировать запросы ORM Django — only(), defer(), values(), annotate()?
only()/defer() — загружают часть полей с инстансами модели; values()/values_list() — возвращают словари/кортежи без объектов; annotate() добавляет вычисляемые агрегаты прямо в SQL, избавляя от N+1 на подсчётах.
Оптимизация QuerySet в Django
Django ORM предоставляет несколько инструментов для сокращения объёма данных, передаваемых из БД: only(), defer(), values(), values_list() и annotate(). Каждый решает свою задачу.
only() — загружать только указанные поля
Возвращает полноценные экземпляры модели, но с отложенной загрузкой остальных полей (Deferred Attributes). При доступе к отложенному полю Django выполнит дополнительный запрос.
from myapp.models import Article
# SELECT id, title FROM articles
articles = Article.objects.only("id", "title")
for a in articles:
print(a.title) # без доп. запроса
print(a.body) # ВНИМАНИЕ: новый запрос SELECT body FROM articles WHERE id=X
Используйте only() когда нужны объекты модели (например, для передачи в функции, ожидающие инстанс), но не все поля.
defer() — исключить указанные поля
Инверсия only(): загружает все поля кроме перечисленных.
# SELECT id, title, status, published_at FROM articles (без body — большой TextFie d)
articles = Article.objects.defer("body")
for a in articles:
print(a.title) # без доп. запроса
print(a.body) # доп. запрос
defer() удобен, когда таблица имеет одно-два «тяжёлых» поля (TEXT, JSONB, BYTEA), которые не нужны в списочном представлении.
values() — словари вместо объектов модели
Возвращает ValuesQuerySet — итератор словарей. Нет накладных расходов на создание Python-объектов модели. Идеален для API и агрегаций.
# SELECT id, title FROM articles WHERE status='published'
articles = Article.objects.filter(status="published").values("id", "title")
# [{"id": 1, "title": "Hello"}, ...]
# Доступ к FK:
articles = Article.objects.values("id", "title", "author__name")
# SELECT articles.id, articles.title, authors.name FROM articles JOIN authors...
values_list() — кортежи или скалярные значения
# Список кортежей:
Article.objects.values_list("id", "title")
# [(1, "Hello"), (2, "World"), ...]
# Плоский список (flat=True, только одно поле):
ids = list(Article.objects.filter(status="published").values_list("id", flat=True))
# [1, 2, 3, ...]
# Именованные кортежи:
Article.objects.values_list("id", "title", named=True)
annotate() — вычисляемые поля в SQL
Добавляет агрегированные или вычисляемые значения прямо в QuerySet через SQL-функции. Избегает N+1 при подсчёте связанных объектов.
from django.db.models import Count, Avg, F, ExpressionWrapper, DurationField
from django.utils import timezone
# Количество комментариев к каждой статье
articles = (
Article.objects
.annotate(comment_count=Count("comments"))
.order_by("-comment_count")
)
for a in articles:
print(a.title, a.comment_count)
# Средний рейтинг по автору
from myapp.models import Author
authors = Author.objects.annotate(avg_rating=Avg("articles__rating"))
# Вычисление выражений через F и ExpressionWrapper
articles = Article.objects.annotate(
age=ExpressionWrapper(
timezone.now() - F("published_at"),
output_field=DurationField()
)
)
Сравнение подходов
only()/defer()— объекты модели, меньше полей. Риск N+1 при доступе к отложенным полям.values()/values_list()— словари/кортежи, минимум памяти, нет методов модели.annotate()— вычисления в SQL, устраняет N+1 для агрегаций.
Подводные камни
only()безselect_related()для FK-поля создаёт N+1: поле id загружено, но при доступе кarticle.authorDjango делает отдельный запрос.defer()на поле, используемое вorder_by()илиfilter(), всё равно попадает в SQL — Django добавляет его автоматически.values()возвращает FK какauthor_id(число), а неauthor(объект). Для доступа к полю связанной модели нуженauthor__name.annotate(Count("comments"))сfilter(comments__status="approved")создаст считаться только одобренных комментариев — это может быть неожиданным: используйтеCount("comments", filter=Q(comments__status="approved")).- Несколько
annotate(Count(...))на разных M2M могут давать декартово произведение — разбивайте на отдельные подзапросы черезSubquery. values()+annotate()группирует по всем перечисленным полямvalues(), аналогично SQL GROUP BY.- Возвращаемые
values()-словари не являются инстансами модели: методы модели,save(), сигналы — ничего из этого недоступно.
Common mistakes
- Описывать queryset optimization только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом Django и реальной эксплуатацией.
What the interviewer is testing
- Объясняет queryset optimization через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.