annotate, aggregate, Q и F
Считать суммы, средние и количества лучше в базе, а не в Python. Для этого в ORM есть aggregate, annotate, F и Q — и каждый закрывает свой класс задач.
aggregate сводит всю выборку к одному набору чисел, annotate добавляет вычисленное значение к каждой строке, F ссылается на поле прямо в SQL, а Q собирает условия с
ORиNOT.
Соблазн посчитать «среднюю цену» или «число заказов на клиента» циклом в Python велик, но это и медленно, и снова приводит к N+1. СУБД умеет агрегировать встроенно и быстро — ORM лишь даёт удобный мост к этим возможностям.
aggregate: одно число на всю выборку
aggregate() применяет агрегатную функцию ко всему QuerySet и возвращает словарь с результатом. Это терминальная операция — она сразу выполняет запрос.
from django.db.models import Count, Sum, Avg, Min, Max
Order.objects.aggregate(Avg("total"))
# {"total__avg": 523.4}
Order.objects.aggregate(
orders=Count("id"),
revenue=Sum("total"),
biggest=Max("total"),
)
# {"orders": 1240, "revenue": 648_900, "biggest": 9_800}
Ключи можно назвать самому (orders=...), иначе Django сгенерирует имя вида поле__функция. Один SELECT с COUNT/SUM вместо выгрузки тысяч строк в Python.
annotate: значение на каждую строку (группировка)
Если нужно не одно число на всё, а по числу для каждого объекта — это annotate(). Он добавляет к каждой строке вычисленное «виртуальное поле». Классика — количество связанных объектов:
from django.db.models import Count
authors = Author.objects.annotate(num_articles=Count("articles"))
for a in authors:
print(a.name, a.num_articles) # число статей доступно как обычный атрибут
За кулисами это GROUP BY author.id с COUNT по статьям — один запрос вместо N. Аннотированное поле полноценно: по нему можно фильтровать и сортировать.
Author.objects.annotate(num=Count("articles")) \
.filter(num__gte=10) \
.order_by("-num")
# авторы с 10+ статьями, по убыванию числа статей
Важно различать момент фильтрации. Условие до агрегата кладут в filter(...) перед annotate — оно попадёт в WHERE. Условие на сам агрегат — в filter() после annotate — оно попадёт в HAVING. Можно фильтровать и внутри самого Count через аргумент filter=:
from django.db.models import Count, Q
Author.objects.annotate(
published=Count("articles", filter=Q(articles__published=True))
)
# у каждого автора — число именно опубликованных статей
F-выражения: операции прямо в базе
F ссылается на значение поля в момент выполнения SQL, не вытаскивая его в Python. Это даёт две вещи: операции «поле относительно самого себя» и сравнение двух полей одной строки.
from django.db.models import F
# увеличить просмотры на 1 — БЕЗ гонки данных
Article.objects.filter(pk=10).update(views=F("views") + 1)
# SQL: UPDATE article SET views = views + 1 WHERE id = 10
Почему это лучше, чем a.views += 1; a.save()? Питоновский вариант читает текущее значение, прибавляет в памяти и записывает — между чтением и записью другой процесс может изменить счётчик, и инкремент потеряется. F("views") + 1 выполняет сложение атомарно внутри СУБД, гонки нет. Плюс это один запрос на все строки, а не цикл.
# сравнить два поля одной строки
Product.objects.filter(sold__gt=F("stock"))
# товары, проданных больше, чем было на складе (аномалия)
# арифметика между полями в аннотации
Product.objects.annotate(remaining=F("stock") - F("sold"))
Q-объекты: сложные условия с OR и NOT
Аргументы filter() всегда соединяются через AND. Чтобы получить OR, отрицание или скобки, нужны Q-объекты. Их комбинируют операторами | (ИЛИ), & (И) и ~ (НЕ).
from django.db.models import Q
# опубликованные ИЛИ закреплённые
Article.objects.filter(Q(published=True) | Q(pinned=True))
# (django ИЛИ python) И НЕ забанен автор
Article.objects.filter(
(Q(tags__name="django") | Q(tags__name="python")) & ~Q(author__is_banned=True)
)
Q можно смешивать с обычными именованными аргументами, но Q-объекты идут первыми: filter(Q(a) | Q(b), is_active=True). Это удобно для динамического поиска — условия собирают в переменную и комбинируют по флагам формы.
query = Q()
if search_text:
query &= (Q(title__icontains=search_text) | Q(body__icontains=search_text))
if only_published:
query &= Q(published=True)
Article.objects.filter(query) # условие собрано динамически
SQL под капотом
Аннотация с группировкой превращается примерно в такой запрос — приведён для чтения, чтобы видеть GROUP BY:
SELECT author.id, author.name, COUNT(article.id) AS num_articles
FROM author
LEFT JOIN article ON article.author_id = author.id
GROUP BY author.id, author.name
HAVING COUNT(article.id) >= 10
ORDER BY num_articles DESC;
Видно, что annotate(Count(...)) даёт GROUP BY + агрегат, фильтр на агрегат уходит в HAVING, а сортировка по аннотации работает как по обычной колонке.
Как это работает под капотом
Все эти выражения — Count, Sum, F, Q — это узлы того же дерева Query, что и filter. ORM не вычисляет их в Python; он транслирует их в SQL и поручает счёт самой СУБД, которая делает это на порядки быстрее, да ещё и не гоняет данные по сети. annotate добавляет агрегат в SELECT и при необходимости GROUP BY; одновременное использование нескольких агрегатов или комбинация annotate с несколькими JOIN может приводить к «двойному счёту» (декартово произведение строк), и тогда внутри агрегата ставят distinct=True. F компилируется в ссылку на колонку, поэтому выражение F("a") + F("b") становится частью SQL и вычисляется построчно базой. Q — это просто узел дерева условий, который умеет соединяться булевыми операторами и переворачиваться через ~.
Частые ошибки
- Считать агрегаты циклом в Python. Перебрать тысячи строк ради суммы — это и медленно, и часто N+1; для этого есть
aggregate/annotate, считающие в базе. - Путать
aggregateиannotate. Первый сводит всю выборку к словарю чисел, второй добавляет число к каждой строке. Нужно «по объекту» — этоannotate. - Делать инкремент как
obj.x += 1; obj.save(). Между чтением и записью возможна гонка;update(x=F("x") + 1)атомарен в СУБД и безопасен при конкуренции. - Ждать
ORот обычных аргументовfilter(). Они всегдаAND; дляOR/NOT/скобок нужныQ-объекты с|,&,~. - Завышенные числа в аннотации при нескольких связях. Несколько JOIN раздувают строки и агрегат считает лишнее; лечится
distinct=TrueвнутриCount.
Итоги
aggregateвозвращает словарь сводных чисел по всей выборке за одинSELECT.annotateдобавляет вычисленное поле к каждой строке (часто черезGROUP BY); по нему можно фильтровать и сортировать.- Фильтр до агрегата уходит в
WHERE, фильтр на сам агрегат — вHAVING; точечный фильтр внутри агрегата задаётся аргументомfilter=Q(...). Fвыполняет операции и сравнения полей прямо в базе — это и атомарность (без гонок при инкременте), и один запрос вместо цикла.QдаётOR,NOTи скобки через|,&,~и удобен для динамической сборки условий.