В предыдущем уроке мы научились работать с агрегирующими функциями — считать, суммировать, вычислять средние значения.
Теперь мы сделаем следующий шаг и научимся группировать записи: собирать данные по категориям, жанрам, годам и любым другим признакам.
Группировка — это один из важнейших инструментов аналитики. Она лежит в основе:
- статистики,
- дашбордов,
- аналитических отчетов,
- страниц каталога,
- фильтров и витрин данных.
В Django за группировку отвечает связка:
values() + annotate()
Причём порядок важен: сначала определяется, по каким полям группировать, а затем — какие агрегаты вычислять для каждой группы.
Представьте задачи:
- Посчитать, сколько фильмов относится к каждому жанру.
- Узнать, сколько фильмов выпущено в каждый год.
- Посчитать количество фильмов у каждого режиссёра.
- Понять, какие теги используются чаще остальных.
Все эти задачи не требуют получения всех фильмов — группировка происходит прямо в базе данных.
Предположим, у нас есть модели:
class Genre(models.Model):
name = models.CharField(max_length=100)
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.IntegerField()
genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True)
rating = models.FloatField()from django.db.models import Count
Movie.objects.values("genre__name").annotate(total=Count("id"))Что происходит?
values("genre__name")— группируем по жанрам.annotate(total=Count("id"))— для каждой группы считаем фильмы.
SQL, который построит Django:
SELECT genre.name, COUNT(movie.id)
FROM movie
LEFT JOIN genre ON movie.genre_id = genre.id
GROUP BY genre.name;Результат будет выглядеть так:
[
{"genre__name": "Комедия", "total": 12},
{"genre__name": "Драма", "total": 8},
{"genre__name": "Боевик", "total": 5},
]Неправильно:
Movie.objects.annotate(total=Count("id")).values("genre__name")Почему ошибка?
Потому что:
- сначала будет выполнена аннотация по всей таблице,
- а группировка произойдёт позже,
- что приведёт к неверному результату (одно число вместо групп).
Правильно:
Movie.objects.values("genre__name").annotate(total=Count("id"))Запомните правило:
Группировка — это values()
Аналитика по группам — это annotate()
По умолчанию Django назовёт поле:
id__count
Но лучше задавать имя явно:
Movie.objects.values("genre__name").annotate(total=Count("id"))Или ещё лучше:
Movie.objects.values("genre__name").annotate(
movies_count=Count("id")
)Так читается намного понятнее.
Иногда нам нужны не все группы, а только те, которые удовлетворяют условиям.
Genre.objects.annotate(total=Count("movie")).filter(total__gt=0)Django сам понимает, что:
- таблица
movieссылается наgenre, - связь называется
movie_set(илиmovieпри related_name), - группировка идёт по жанрам.
Movie.objects.values("year").annotate(
total=Count("id")
).filter(total__gt=3)Результат:
[
{"year": 2019, "total": 7},
{"year": 2021, "total": 4}
]Создадим временную вьюшку:
def genre_stats(request):
data = Movie.objects.values("genre__name").annotate(
total=Count("id")
)
return HttpResponse(str(list(data)))Открываем в браузере:
[{'genre__name': 'Комедия', 'total': 12},
{'genre__name': 'Драма', 'total': 8},
{'genre__name': 'Боевик', 'total': 5}]
Если видите ошибку:
- Проверьте, есть ли жанры.
- Проверьте, заполнены ли фильмы.
- Проверьте, что миграции применены.
Кроме Count(), можно использовать:
- Avg
- Sum
- Max
- Min
- Length
- Upper, Lower
- ExtractYear и другие функции дат
Например:
from django.db.models.functions import Length
Movie.objects.annotate(name_len=Length("title"))Movie.objects.values("genre__name").annotate(
avg_rating=Avg("rating")
)Movie.objects.values("genre__name").annotate(
oldest=Min("year")
)Вы указали неправильное имя поля.
Например:
Movie.objects.values("genres__name")А правильное:
Movie.objects.values("genre__name")Movie.objects.annotate(total=Count("id")).values("genre__name")Решение:
Movie.objects.values("genre__name").annotate(total=Count("id"))Если у модели обратная связь называется movie_set, а вы пишете:
Genre.objects.annotate(total=Count("movies"))Будет ошибка. Используйте:
Genre.objects.annotate(total=Count("movie"))или задайте related_name:
genre = models.ForeignKey(..., related_name="movies")Сгруппируйте фильмы по году выпуска и подсчитайте количество фильмов в каждом году.
Сделайте группировку по жанрам и вычислите для каждого жанра:
- количество фильмов,
- средний рейтинг,
- минимальный год выпуска.
Используйте annotate().
Найдите только те жанры, у которых есть фильмы.
Используйте Genre.objects.annotate().
Получите список:
{"year": ..., "avg_rating": ...}
для каждого года, где средняя оценка выше 7.
Во временной вьюшке выведите список жанров вместе с количеством фильмов.
Проверьте работу в браузере.
Аннотируйте каждому фильму длину названия и отсортируйте по этому значению.
- Что делает связка values() + annotate()?
- Почему порядок values() → annotate() важен?
- Что происходит, если использовать annotate() до values()?
- Как получить количество фильмов в каждом жанре?
- Как фильтровать сгруппированные данные?
- Что делает функция Length()?
- Как сгруппировать фильмы по году?
- Как работает related_name при группировке?
- Что вернёт запрос:
Movie.objects.values("genre__name").annotate(avg=Avg("rating"))? - Почему группировка выполняется на уровне базы данных, а не Python?