Как фильтровать объекты для аннотации count в Django?
Рассмотрим простые модели Django Event
и Participant
:
class Event(models.Model):
title = models.CharField(max_length=100)
class Participant(models.Model):
event = models.ForeignKey(Event, db_index=True)
is_paid = models.BooleanField(default=False, db_index=True)
Легко аннотировать запрос событий с общим количеством участников:
events = Event.objects.all().annotate(participants=models.Count('participant'))
Как комментировать количество участников, отфильтрованных по is_paid=True
?
Мне нужно запрашивать все события независимо от количества участников, например, мне не нужно фильтровать по аннотированному результату. Если есть 0
участников, это нормально, мне просто нужно 0
в аннотированном значении.
Пример из документации здесь не работает, потому что он исключает объекты из запроса вместо того, чтобы пометить их 0
.
Обновить. В Django 1.8 появилась новая функция условных выражений, так что теперь мы можем сделать так:
events = Event.objects.all().annotate(paid_participants=models.Sum(
models.Case(
models.When(participant__is_paid=True, then=1),
default=0,
output_field=models.IntegerField()
)))
Обновление 2. В Django 2.0 появилась новая функция условного агрегирования, см. Принятый ответ ниже.
Ответы
Ответ 1
Условное агрегирование в Django 2.0 позволяет вам еще больше уменьшить количество ошибок, которые были в прошлом. Это также будет использовать логику filter
Postgres, которая работает несколько быстрее, чем суммированный регистр (я видел числа, например, 20-30%).
В любом случае, в вашем случае мы рассматриваем что-то простое:
from django.db.models import Q, Count
events = Event.objects.annotate(
paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)
В документации есть отдельный раздел о фильтрации аннотаций. Это то же самое, что и условная агрегация, но больше похоже на мой пример выше. В любом случае, это намного полезнее, чем грубые подзапросы, которые я делал раньше.
Ответ 2
Только что обнаружил, что Django 1.8 имеет новую функцию условных выражений , теперь мы можем сделать вот так:
events = Event.objects.all().annotate(paid_participants=models.Sum(
models.Case(
models.When(participant__is_paid=True, then=1),
default=0, output_field=models.IntegerField()
)))
Ответ 3
UPDATE
Подпоследовательный подход, о котором я упоминаю, теперь поддерживается в Django 1.11 через subquery-expressions.
Event.objects.annotate(
num_paid_participants=Subquery(
Participant.objects.filter(
is_paid=True,
event=OuterRef('pk')
).values('event')
.annotate(cnt=Count('pk'))
.values('cnt'),
output_field=models.IntegerField()
)
)
Я предпочитаю эту агрегацию (сумма + случай), потому что она должна быть быстрее и проще для оптимизации (с правильной индексацией).
Для более старой версии это может быть достигнуто с помощью .extra
Event.objects.extra(select={'num_paid_participants': "\
SELECT COUNT(*) \
FROM `myapp_participant` \
WHERE `myapp_participant`.`is_paid` = 1 AND \
`myapp_participant`.`event_id` = `myapp_event`.`id`"
})
Ответ 4
Я бы предложил использовать .values
метод вашего запроса Participant
.
Короче говоря, что вы хотите сделать:
Participant.objects\
.filter(is_paid=True)\
.values('event')\
.distinct()\
.annotate(models.Count('id'))
Полный пример следующий:
-
Создайте 2 Event
s:
event1 = Event.objects.create(title='event1')
event2 = Event.objects.create(title='event2')
-
Добавьте Participant
к ним:
part1l = [Participant.objects.create(event=event1, is_paid=((_%2) == 0))\
for _ in range(10)]
part2l = [Participant.objects.create(event=event2, is_paid=((_%2) == 0))\
for _ in range(50)]
-
Группировать все Participant
по полю Event
:
Participant.objects.values('event')
> <QuerySet [{'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, '...(remaining elements truncated)...']>
Здесь необходимы разные символы:
Participant.objects.values('event').distinct()
> <QuerySet [{'event': 1}, {'event': 2}]>
Что делают .values
и .distinct
, так это то, что они создают два ведра Participant
, сгруппированные по их элементу Event
. Обратите внимание, что эти ведра содержат Participant
.
-
Затем вы можете аннотировать эти ведра, поскольку они содержат набор оригинальных Participant
. Здесь мы хотим подсчитать количество Participant
, это просто делается путем подсчета id
элементов в этих ковшиках (поскольку они Participant
):
Participant.objects\
.values('event')\
.distinct()\
.annotate(models.Count('id'))
> <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
-
Наконец, вы хотите только Participant
с is_paid
, являющимся True
, вы можете просто добавить фильтр перед предыдущим выражением, и это даст выражение, показанное выше:
Participant.objects\
.filter(is_paid=True)\
.values('event')\
.distinct()\
.annotate(models.Count('id'))
> <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]>
Единственный недостаток заключается в том, что вам нужно снова получить Event
, поскольку у вас есть только id
из метода выше.
Ответ 5
Какой результат я ищу:
- Люди (уполномоченные), у которых есть задачи, добавленные в отчет. - Общее количество уникальных людей
- Люди, у которых есть задачи, добавленные в отчет, но, для задачи, чья способность счета больше, чем 0.
В общем, мне пришлось бы использовать два разных запроса:
Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()
Но я хочу оба в одном запросе. Следовательно:
Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))
Результат:
<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>