Работа с данными практически всегда предполагает не только выборку, но и аналитику. Сколько фильмов в базе? Какой средний рейтинг? Какое количество фильмов относится к определённому жанру? Какие годы выпуска встречаются чаще?
Django ORM предоставляет мощные инструменты для вычислений прямо на уровне базы данных — агрегирующие функции и метод values().
В этом уроке мы научимся работать с аналитикой, разберём типовые ошибки и проведём несколько небольших проверок через браузер.
Агрегации — это операции, которые выполняются над группой записей, например:
- Сколько записей?
- Какое минимальное значение?
- Какой средний рейтинг?
- Какова общая сумма бюджетов?
В Django агрегирующие функции находятся в модуле:
from django.db.models import Count, Sum, Avg, Max, MinИ применяются с помощью метода aggregate(), который всегда возвращает один результат, упакованный в словарь.
Например:
Movie.objects.aggregate(Count("id"))Вернёт словарь:
{"id__count": 57}Так работает Django: имя ключа создаётся автоматически — имяполя + __ + функция_.
#3 2. Метод count(): самый простой способ посчитать записи
Метод count() — это специальный «быстрый путь» для подсчёта количества строк.
Например, подсчитаем количество фильмов:
Movie.objects.count()SQL, который построит Django:
SELECT COUNT(*) AS "__count" FROM "cinemahub_movie";- Django не загружает записи в память.
- Подсчёт выполняется на уровне базы (быстро и эффективно).
- Безопасно для больших таблиц.
Если вы когда-то встречали код вроде:
len(Movie.objects.all())— это антипаттерн. Такой код загружает ВСЕ фильмы в память, а затем считает длину списка.
Предположим, у нас есть модель:
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.IntegerField()
rating = models.FloatField()
budget = models.IntegerField(null=True, blank=True)
genre = models.ForeignKey("Genre", on_delete=models.SET_NULL, null=True)Movie.objects.aggregate(Min("year"))Результат:
{"year__min": 1979}Movie.objects.aggregate(Max("rating"))Movie.objects.aggregate(
min_budget=Min("budget"),
max_budget=Max("budget")
)Movie.objects.aggregate(Avg("rating"))Если в таблице много NULL-значений, Django будет игнорировать их — агрегаты работают корректно.
Movie.objects.aggregate(Sum("budget"))Movie.objects.aggregate(
Sum("budget"),
Avg("rating"),
Max("year"),
Min("year"),
)Можно считать этот метод маленьким аналитическим отчётом.
Django ORM поддерживает арифметику прямо внутри агрегирующих выражений.
Например, вычислим разницу между суммой и средним бюджетом:
Movie.objects.aggregate(
diff=Sum("budget") - Avg("budget")
)Или, например, вычислим среднюю стоимость оценки:
Movie.objects.aggregate(
value=Avg("budget") / Avg("rating")
)Важно: Если вы используете выражение, обязательно назначайте имя:
Movie.objects.aggregate(result=...)Иначе Django просто не знает, какой ключ использовать.
Как и в обычных запросах, вы можете использовать .filter():
Например, количество фильмов с рейтингом выше 8:
Movie.objects.filter(rating__gt=8).aggregate(Count("id"))Или сумма бюджетов жанра «Комедия»:
Movie.objects.filter(
genre__name="Комедия"
).aggregate(Sum("budget"))Метод values() позволяет запрашивать только часть колонок — не всю строку.
Например, выберем только названия и годы:
Movie.objects.values("title", "year")Результат:
[
{"title": "Inception", "year": 2010},
{"title": "Interstellar", "year": 2014},
]Это удобно, когда:
- Вы делаете отчёты,
- Готовите данные под JSON,
- Вам не нужна вся модель.
Запросим название фильма и название жанра:
Movie.objects.values("title", "genre__name")Django построит SQL с JOIN:
SELECT movie.title, genre.name
FROM movie
JOIN genre ON movie.genre_id = genre.id;Очень важно понимать, что пока вы не обратились к данным, SQL-запрос не выполнен.
movies = Movie.objects.values("title")На этом шаге — запрос не отправлен. Но когда вы пройдёте по QuerySet в цикле:
for m in movies:
print(m["title"])Django выполнит запрос ровно один раз.
Это делает ORM очень эффективным: вы можете собирать сложные цепочки фильтраций и аннотаций, и только в конце данные действительно загружаются.
Для простейшей проверки можно создать временную вьюшку:
def stats(request):
data = Movie.objects.aggregate(
total=Count("id"),
avg_rating=Avg("rating"),
max_year=Max("year")
)
return HttpResponse(str(data))После обновления страницы в браузере вы увидите:
{"total": 57, "avg_rating": 7.4, "max_year": 2023}
Убедитесь, что вы:
- Перешли по правильному URL.
- Действительно создали фильмы в базе.
- Не забыли миграции.
Причина: попытка сложить два агрегата без имени.
Правильно:
Movie.objects.aggregate(result=Sum("budget") - Avg("budget"))Неправильно:
Movie.objects.aggregate(Sum("budget") - Avg("budget"))len(Movie.objects.all()) # плохо
Movie.objects.count() # правильноНекоторые новички ожидают:
movies = Movie.objects.values("title")
print(movies[0].title) # AttributeErrorПравильный доступ:
movies[0]["title"]Все задания можно выполнить через Django shell или временную вьюшку.
Посчитайте количество фильмов, выпущенных после 2010 года.
Найдите:
- минимальный рейтинг
- максимальный рейтинг
- средний рейтинг
Получите список словарей вида:
{"title": "...", "genre__name": "..."}Подсчитайте количество фильмов в каждом жанре (используйте values() + annotate()).
Вычислите:
(средний бюджет всех фильмов) - (минимальный бюджет)
Получите список всех годов выпуска без повторений с помощью:
Movie.objects.values("year")И попробуйте вывести их в браузер.
Подсчитайте количество фильмов с рейтингом выше среднего.
Найдите общую сумму бюджетов всех фильмов жанра «боевик».
- Что делает метод
aggregate()и что он возвращает? - Почему
count()эффективнее, чемlen(QuerySet)? - Что вернёт
Movie.objects.aggregate(Max("rating"))? - Как получить только поля
titleиyearбез загрузки модели целиком? - Что делает конструкция
genre__nameв методеvalues()? - Происходит ли выполнение SQL-запроса при создании QuerySet?
- Почему
values()возвращает dict, а не объект модели? - Можно ли использовать арифметические операции в
aggregate()? - Что произойдёт, если попытаться использовать выражение в
aggregate()без имени? - В чём разница между
count()иCount()?