Агрегация является источником путаницы в любом типе ORM, и Django ничем не отличается. Документация содержит множество примеров и шпаргалок, которые демонстрируют, как группировать и агрегировать данные с помощью ORM, но я решил подойти к этому с другой стороны.
В этой статье я поставил QuerySets и SQL рядом и рассказать о каждом типе группировке отдельно.
Table of Contents
Для демонстрации различных запросов GROUP BY я буду использовать модель User из встроенного в Django приложения django.contrib.auth.
>>> from django.contrib.auth.models import User
Django ORM создает операторы SQL с длинными псевдонимами. Для краткости я буду показывать очищенную, но эквивалентную версию того, что использует Django.
SQL LOGGING
Чтобы увидеть, какой SQL фактически выполняется в Django, вы можете включить ведение журнала SQL в настройках Django (turn on SQL logging in the Django settings).
Давайте посчитаем, сколько у нас пользователей:
>>> User.objects.count() 20
SELECT COUNT(*) FROM auth_user;
Подсчет строк настолько распространен, что Django включает специальную функцию count для него прямо в QuerySet. В отличие от других QuerySets, которые мы увидим далее, count возвращает число.
Django предлагает еще два способа подсчета строк в таблице.
Начнем с aggregate
:
from django.db.models import Count >>> User.objects.aggregate(Count('id')) {'id__count': 20}
SELECT COUNT(id) AS id__count FROM auth_user;
Для использования aggregate мы импортировали функцию агрегирования Count. Функция принимает выражение для подсчета. В нашем случае мы использовали имя идентификатора столбца id для подсчета всех строк в таблице.
AGGREGATE NULL
Агрегаты игнорируют значения NULL. Подробнее о том, как агрегаты обрабатывают NULL, см. В разделе 12 Common Mistakes and Missed Optimization Opportunities in SQL.
Результатом aggregate
будет словарь:
>>> from django.db.models import Count >>> User.objects.aggregate(Count('id')) {'id__count': 20}
Имя ключа происходит от имени поля и имени агрегата. В данном случае это id__count. Рекомендуется не полагаться на это соглашение об именах, а вместо этого указывать свое имя:
SELECT COUNT(id) as total FROM auth_user;
>>> from django.db.models import Count >>> User.objects.aggregate(total=Count('id')) {'total': 20}
Имя аргумента для aggregate — это также имя ключа в результирующем словаре.
Используя aggregate, мы получили результат применения агрегатной функции ко всей таблице. Это полезно, но обычно мы хотим применить агрегацию к группам строк.
Давайте посчитаем пользователей по их активному статусу:
(User.objects .values('is_active') .annotate(total=Count('id'))) <QuerySet [{'is_active': True, 'total': 20}]>
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active
На этот раз мы использовали функцию annotate
. Для создания GROUP BY мы используем комбинацию values и annotate:
values('is_active')
: что группироватьannotate(total=Count('id'))
: что агрегироватьПорядок важен: невозможность вызова values до annotate не приведет к агрегированным результатам.
Как и aggregate, имя аргумента для annotate является ключом в результате оцененного QuerySet. В этом случае это total.
Чтобы применить агрегацию к отфильтрованному запросу, вы можете использовать filter
в любом месте запроса. Например, учитывайте только штатных пользователей по их активному статусу:
(User.objects .values('is_active') .filter(is_staff=True) .annotate(total=Count('id'))) <QuerySet [{'is_active': True, 'total': 1}]>
SELECT is_active, COUNT(id) AS total FROM auth_user WHERE is_staff = True GROUP BY is_active
Как и filter, для сортировки набора запросов используйте order_by в любом месте запроса:
(User.objects .values('is_active') .annotate(total=Count('id')) .order_by('is_staff', 'total')) <QuerySet [{'is_active': True, 'total': 19}, {'is_active': True, 'total': 1}]>
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active ORDER BY is_active, total
Обратите внимание, что вы можете сортировать как по ключу GROUP BY, так и по полю агрегирования.
Чтобы создать несколько агрегатов одной группы, добавьте несколько аннотаций:
from django.db.models import Max (User.objects .values('is_active') .annotate( total=Count('id'), last_joined=Max('date_joined'), )) <QuerySet [{'is_active': True, 'total': 20, 'last_joined': datetime.datetime(2020, 2, 20, 12, 40, 28, tzinfo=<UTC>)}]>
SELECT is_active, COUNT(id) AS total, MAX(date_joined) AS last_joined FROM auth_user GROUP BY is_active
В запросе будет указано количество активных и неактивных пользователей и последняя дата, когда пользователь присоединился к каждой группе.
Так же, как и при выполнении нескольких агрегаций, мы также можем сгруппировать по нескольким полям. Например, сгруппировать по активному статусу и статусу персонала:
(User.objects .values('is_active', 'is_staff') .annotate(total=Count('id'))) <QuerySet [{'is_active': True, 'is_staff': False, 'total': 19}, {'is_active': True, 'is_staff': True, 'total': 1}]>
SELECT is_active, is_staff, COUNT(id) AS total FROM auth_user GROUP BY is_active, is_staff
Результат этого запроса включает is_active, is_staff и количество пользователей в каждой группе.
Другим распространенным вариантом использования GROUP BY является группирование по выражению. Например, подсчет количество пользователей, которые присоединились каждый год:
(User.objects .values('date_joined__year') .annotate(total=Count('id'))) <QuerySet [{'date_joined__year': 2020, 'total': 20}]>
SELECT EXTRACT('year' FROM date_joined), COUNT(id) AS total FROM auth_user GROUP BY EXTRACT('year' FROM date_joined)
Обратите внимание, что для получения года от даты мы использовали специальное выражение <field>__year при первом вызове values(). Результатом запроса является dict, а имя ключа будет date_joined__year.
Иногда встроенных выражений недостаточно, и вам нужно объединиться в более сложное выражение. Например, группировка по пользователям, которые вошли в систему с момента регистрации:
from django.db.models import ( ExpressionWrapper, Q, F, BooleanField, ) (User.objects .annotate( logged_since_joined=ExpressionWrapper( Q(last_login__gt=F('date_joined')), output_field=BooleanField(), ) ) .values('logged_since_joined') .annotate(total=Count('id')) .values('logged_since_joined', 'total')) <QuerySet [{'logged_since_joined': 2, 'total': 19}, {'logged_since_joined': True, 'total': 1}]>
SELECT last_login > date_joined AS logged_since_joined, COUNT(id) AS total FROM auth_user GROUP BY last_login > date_joined
Выражение здесь довольно сложное. Сначала мы используем annotate для построения выражения и помечаем его как ключ GROUP BY, ссылаясь на выражение через logged_since_joined в следующем вызове values(). А далее уже действуем как обычно.
Используя условное агрегирование, вы можете агрегировать только часть группы. Условия пригодятся, когда у вас есть несколько агрегатов. Например, подсчитайте количество сотрудников и пользователей, не являющихся сотрудниками, за год, когда они зарегистрировались:
from django.db.models import F, Q (User.objects .values('date_joined__year') .annotate( staff_users=( Count('id', filter=Q(is_staff=True)) ), non_staff_users=( Count('id', filter=Q(is_staff=False)) ), )) <QuerySet [{'date_joined__year': 2020, 'staff_users': 1, 'non_staff_users': 19}]>
SELECT EXTRACT('year' FROM date_joined), COUNT(id) FILTER ( WHERE is_staff = True ) AS staff_users, COUNT(id) FILTER ( WHERE is_staff = False ) AS non_staff_users FROM auth_user GROUP BY EXTRACT('year' FROM date_joined)
Приведенный выше SQL взят из PostgreSQL, который наряду с SQLite в настоящее время является единственным бэкэндом базы данных, который поддерживает ярлык синтаксиса FILTER (формально называемый «выборочные агрегаты» «selective aggregates»). Для других серверных баз данных ORM вместо этого будет использовать CASE … WHEN.
Совет
Я ранее писал про агрегаты с фильтрами. Посмотрите мои 9 советов по Django для работы с базами данных (9 Django tips for working with databases).
Предложение HAVING используется для фильтрации результатов статистической функции. Например, найдите годы, в которые вступили более 100 пользователей:
(User.objects .annotate(year_joined=F('date_joined__year')) .values('is_active') .annotate(total=Count('id')) .filter(total__gt=100))
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active HAVING COUNT(id) > 100
Фильтр по аннотированному итоговому полю total добавил предложение HAVING в сгенерированный SQL.
Для некоторых агрегатных функций, таких как COUNT, иногда желательно учитывать только определенные случаи. Например, сколько разных фамилий существует для каждого активного статуса пользователя:
(User.objects .values('is_active') .annotate( total=Count('id'), unique_names=Count('last_name', distinct=True), )) <QuerySet [{'is_active': True, 'total': 20, 'unique_names': 7}]>
SELECT is_active, COUNT(id) AS total, COUNT(DISTINCT last_name) AS unique_names FROM auth_user GROUP BY is_active
Обратите внимание на использование distinct=True в вызове Count.
Агрегатные поля часто являются лишь первым шагом к большему вопросу. Например, каков процент уникальных фамилий по активному статусу пользователя:
from django.db.models import FloatField from django.db.models.functions import Cast (User.objects .values('is_active') .annotate( total=Count('id'), unique_names=Count('last_name', distinct=True), ) .annotate(pct_unique_names=( Cast('unique_names', FloatField()) / Cast('total', FloatField()) ))) <QuerySet [{'is_active': True, 'total': 20, 'unique_names': 7, 'pct_unique_names': 0.35}]>
SELECT is_active, COUNT(id) AS total, COUNT(DISTINCT last_name) AS unique_names, (COUNT(DISTINCT last_name)::float / COUNT(id)::float) AS pct_unique_names FROM auth_user GROUP BY is_active
Первый annotate() определяет совокупные поля. Второй annotate() использует статистическую функцию для создания выражения.
До сих пор мы использовали только данные в одной модели, но агрегаты часто используются в отношениях. Более простой сценарий — отношение «один к одному» или отношение внешнего ключа. Например, скажем, у нас есть UserProfile с отношением «один к одному» с User, и мы хотим подсчитывать пользователей по типу профиля:
(User.objects .values('user_profile__type') .annotate(total=Count('id')))
SELECT p.type, COUNT(u.id) AS total FROM auth_user u JOIN user_profile p ON u.id = p.user_id GROUP BY p.type
Как и выражения GROUP BY, использование отношений в values будет группировать по этому полю. Обратите внимание, что имя типа профиля пользователя в результате будет «user_profile__type».
Более сложный тип отношений — это отношение «многие ко многим». Например, подсчитайте, во сколько групп входит каждый пользователь:
(User.objects .annotate(memberships=Count('groups')) .values('id', 'memberships')) <QuerySet [{'id': 14, 'memberships': 0}, {'id': 17, 'memberships': 0}, {'id': 12, 'memberships': 0}, {'id': 8, 'memberships': 0}, {'id': 15, 'memberships': 0}, {'id': 10, 'memberships': 0}, {'id': 11, 'memberships': 0}, {'id': 18, 'memberships': 0}, {'id': 16, 'memberships': 0}, {'id': 6, 'memberships': 0}, {'id': 19, 'memberships': 0}, {'id': 2, 'memberships': 0}, {'id': 3, 'memberships': 0}, {'id': 23, 'memberships': 0}, {'id': 13, 'memberships': 0}, {'id': 5, 'memberships': 0}, {'id': 22, 'memberships': 0}, {'id': 9, 'memberships': 0}, {'id': 24, 'memberships': 0}, {'id': 7, 'memberships': 0}]>
SELECT u.id, COUNT(ug.group_id) AS memberships FROM auth_user LEFT OUTER JOIN auth_user_groups ug ON ( u.id = ug.user_id ) GROUP BY u.id
Пользователь может быть членом более чем одной группы. Для подсчета количества групп, членом которых является пользователь, мы использовали связанное имя «groups» в модели User. Если связанное имя не задано явно (и явно не отключено), Django автоматически сгенерирует имя в формате {related model model}_set. Например, group_set.
Для более глубокого изучения ORM и GROUP BY, просмотрите следующие ссылки:
Оригинальная статья: Understand Group by in Django with SQL
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Отличная статья!
Но есть вопрос: как быть, когда нужна группировка по полю подзапроса с union? - никак не осилю. annotate после union работать отказывается.
Вроде что то похожее есть на stackoverflow: https://stackoverflow.com/questions/48028411/django-queryset-union-appears-not-to-be-working-when-combined-with-annotate
И есть официальное описание этой проблемы https://code.djangoproject.com/ticket/30659:
Да, annotate () не поддерживается. Согласно документации, "только LIMIT, OFFSET, COUNT (*), ORDER BY и указание столбцов (например, slicing, count (), order_by () и values () / values_list ()] разрешены в результирующем QuerySet. ".
РазбЕраемся.))
Мало быть программистом, надо еще быть грамотным.
ваще не работает
Под заголовком Как сортировать QuerySet с Group By в выводе
оба значения сгруппированы с is_active=True. Наверное, предполагалось, что одно из значений is_active будет равняться False. Ведь группировка должна производиться, в данном случае, по значению True и по False.
Спасибо за статью, было полезно!
Есть опечатки:
в "Как сортировать QuerySet с Group By" .order_by('is_staff', 'total')) вместо is_staff - is_active,
в "Как группировать с «Выражением» (Expression)" во втором примере .values('logged_since_joined', 'total')) лишнее,
в "Как использовать Having" .annotate(year_joined=F('date_joined__year')) лишнее
Отличная статья