В этой статье я поместил QuerySets и SQL рядом. Если вам удобнее всего использовать SQL, это шпаргалка по Django GROUP BY для вас.
- Как сгруппировать в Django
- Как подсчитывать строки
- Как использовать агрегатные функции
- Как сгруппировать
- Как отфильтровать QuerySet с Group By
- Как отсортировать QuerySet с Group By
- Как объединить несколько агрегатов
- Как группировать по нескольким полям
- Как сгруппировать по выражению (Expression)
- Как использовать условное агрегирование
- Как использовать Having
- Как сгруппировать по Distinct
- Как создавать выражения с использованием агрегированных полей
- Как группировать по взаимосвязям
- Как сгруппировать отношения "многие ко многим"
Как сгруппировать `group by` в Django ¶
Чтобы продемонстрировать различные запросы GROUP BY, я буду использовать модели из встроенного в Django приложения django.contrib.auth
.
>>> from django.contrib.auth.models import User
Django ORM создает операторы SQL с длинными псевдонимами. Для краткости я покажу очищенную, но эквивалентную версию того, что выполняет Django.
Логирование SQL
Чтобы увидеть SQL, фактически выполняемый Django, вы можете включить ведение журнала SQL в настройках Django.
Как подсчитывать строки ¶
Посчитаем, сколько у нас пользователей:
SELECT COUNT(*) FROM auth_user;
User.objects.count()
Подсчет строк настолько распространен, что Django включает функцию для этого прямо в QuerySet. В отличие от других QuerySet, которые мы увидим далее, count возвращает число.
Как использовать агрегатные функции ¶
Django предлагает еще два способа подсчета строк в таблице.
Начнем с aggregate:
SELECT COUNT(id) AS id__count FROM auth_user;
from django.db.models import Count User.objects.aggregate(Count('id'))
Чтобы использовать aggregate
, мы импортировали агрегатную функцию Count
. Функция принимает выражение для подсчета. В этом случае мы использовали имя идентификатора столбца первичного ключа для подсчета всех строк в таблице.
Агрегации игнорируют значения NULL.
Результатом агрегирования является словарь:
>>> from django.db.models import Count >>> User.objects.aggregate(Count('id')) {"id__count": 891}
Имя ключа является производным от имени поля и имени агрегата. В данном случае это id__count
. Рекомендуется не полагаться на это соглашение об именах, а вместо этого указать свое собственное имя:
SELECT COUNT(id) as total FROM auth_user;
>>> from django.db.models import Count >>> User.objects.aggregate(total=Count('id')) {"total": 891}
Имя аргумента для агрегирования также является именем ключа в результирующем словаре.
Как делать группировку Group By ¶
Используя aggregate
, мы получили результат применения агрегатной функции ко всей таблице. Это полезно, но обычно мы хотим применить агрегирование к группам строк.
Посчитаем пользователей по их активному статусу:
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active
(User.objects .values('is_active') .annotate(total=Count('id')))
На этот раз мы использовали функцию annotate
. Для создания GROUP BY мы используем комбинацию значений и аннотации:
values('is_active')
: что сгруппироватьannotate(total=Count('id'))
: что агрегировать
Порядок важен: отказ от вызова значений перед аннотацией не приведет к совокупным результатам.
Так же, как и aggregate
, имя аргумента для annotate
является ключом к результату оцененного QuerySet. В данном случае все.
Как отфильтровать QuerySet с помощью Group By ¶
Чтобы применить агрегирование к отфильтрованному запросу, вы можете использовать filter
в любом месте запроса. Например, считать только штатных пользователей по их активному статусу:
SELECT is_active, COUNT(id) AS total FROM auth_user WHERE is_staff = True GROUP BY is_active
(User.objects .values('is_active') .filter(is_staff=True) .annotate(total=Count('id')))
Как отсортировать QuerySet с Group By ¶
Как и фильтр, для сортировки набора запросов используйте order_by
в любом месте запроса:
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active ORDER BY is_active, total
(User.objects .values('is_active') .annotate(total=Count('id')) .order_by('is_staff', 'total'))
Обратите внимание, что вы можете сортировать как по ключу GROUP BY, так и по агрегатному полю.
Как объединить несколько агрегатов ¶
Чтобы создать несколько агрегатов одной и той же группы, добавьте несколько аннотаций:
SELECT is_active, COUNT(id) AS total, MAX(date_joined) AS last_joined FROM auth_user GROUP BY is_active
from django.db.models import Max (User.objects .values('is_active') .annotate( total=Count('id'), last_joined=Max('date_joined'), ))
Запрос будет выдавать количество активных и неактивных пользователей, а также дату последнего присоединения пользователя к каждой группе.
Как группировать по нескольким полям ¶
Как и при выполнении нескольких агрегатов, мы можем также захотеть сгруппировать по нескольким полям. Например, сгруппируйте по активному статусу и статусу персонала:
SELECT is_active, is_staff, COUNT(id) AS total FROM auth_user GROUP BY is_active, is_staff
(User.objects .values('is_active', 'is_staff') .annotate(total=Count('id')))
Результат этого запроса включает is_active
, is_staff
и количество пользователей в каждой группе.
Как сгруппировать по выражению ¶
Другой распространенный вариант использования GROUP BY - группировка по выражению. Например, подсчитайте количество пользователей, которые присоединились по каждому году:
SELECT EXTRACT('year' FROM date_joined), COUNT(id) AS total FROM auth_user GROUP BY EXTRACT('year' FROM date_joined)
(User.objects .values('date_joined__year') .annotate(total=Count('id')))
Обратите внимание, что для получения года с даты мы использовали специальное выражение <field>__year
при первом вызове values()
. Результатом запроса является dict
, а имя ключа будет date_joined__year
.
Иногда встроенных выражений недостаточно, и вам нужно агрегировать более сложное выражение. Например, сгруппируйте пользователей, которые вошли в систему с момента регистрации:
SELECT last_login > date_joined AS logged_since_joined, COUNT(id) AS total FROM auth_user GROUP BY last_login > date_joined
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')
Выражение здесь довольно сложное. Сначала мы используем annotate для построения выражения и помечаем его как ключ GROUP BY, ссылаясь на выражение в следующем вызове values()
. С этого момента все точно так же.
Как использовать условное агрегирование ¶
Используя условное агрегирование, вы можете агрегировать только часть группы. Условия пригодятся, когда у вас есть несколько агрегатов. Например, подсчитайте количество штатных и внештатных пользователей по году их регистрации:
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)
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)) ), ))
Вышеупомянутый SQL взят из PostgreSQL, который наряду с SQLite в настоящее время является единственным сервером базы данных, который поддерживает сокращение синтаксиса FILTER
(формально называемое «выборочные агрегаты»). Для других баз данных вместо этого ORM будет использовать CASE ... WHEN
.
Как использовать Having ¶
Предложение HAVING
используется для фильтрации результата агрегатной функции. Например, найдите годы, в которые присоединились более 100 пользователей:
SELECT is_active, COUNT(id) AS total FROM auth_user GROUP BY is_active HAVING COUNT(id) > 100
(User.objects .annotate(year_joined=F('date_joined__year')) .values('is_active') .annotate(total=Count('id')) .filter(total__gt=100))
The filter on the annotated field total
added an HAVING clause in the generated SQL.
Фильтр по сумме total
аннотированного поля добавил предложение HAVING в сгенерированный SQL.
Как сгруппировать по Distinct ¶
Для некоторых агрегатных функций, таких как COUNT
, иногда желательно подсчитывать только отдельные вхождения. Например, сколько разных фамилий существует для каждого активного статуса пользователя:
SELECT is_active, COUNT(id) AS total, COUNT(DISTINCT last_name) AS unique_names FROM auth_user GROUP BY is_active
(User.objects .values('is_active') .annotate( total=Count('id'), unique_names=Count('last_name', distinct=True), ))
Обратите внимание на использование distinct=True
в вызове Count
.
Как создавать выражения с использованием агрегированных полей ¶
Агрегированные поля часто являются лишь первым шагом к большему вопросу. Например, каков процент уникальных фамилий по активному статусу пользователя:
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
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()) ))
Первый annotate()
определяет поля агрегирования. Второй annotate()
использует агрегатную функцию для создания выражения.
Как группировать по взаимосвязям ¶
До сих пор мы использовали только данные в одной модели, но агрегаты часто используются в отношениях. Более простой сценарий - отношения «один к одному» или внешнего ключа. Например, предположим, что у нас есть UserProfile
с однозначным отношением к пользователю, и мы хотим подсчитывать пользователей по типу профиля:
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
(User.objects .values('user_profile__type') .annotate(total=Count('id')))
Как и в выражениях GROUP BY, использование отношений в values
будет группироваться по этому полю. Обратите внимание, что имя типа профиля пользователя в результате будет «user_profile__type».
Как сгруппировать отношения "многие ко многим" ¶
Более сложный тип отношений - это отношения «многие ко многим». Например, посчитайте, в скольких группах состоит каждый пользователь:
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
(User.objects .annotate(memberships=Count('groups')) .values('id', 'memberships'))
A user can be a member of more than one group. To count the number of groups the user is member of we used the related name "groups" in the User
model. If the related name is not explicitly set (and not explicitly disabled), Django will automatically generate a name in the format {related model model}_set
. For example, group_set
.
Пользователь может быть членом более чем одной группы. Чтобы подсчитать количество групп, в которые входит пользователь, мы использовали связанное имя «groups» в модели пользователя. Если связанное имя не задано явно (и не отключено), Django автоматически сгенерирует имя в формате {related model model}_set
. Например, group_set
.
Перевод https://django.fun/tutorials/ponimanie-group-v-django-s-pomoshyu-sql/