2015-06-10 2 views
47

Рассмотрим простые модели Django Event и Participant:Как фильтровать объекты для аннотации счетчика в Django?

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 в аннотированном значении.

example from documentation здесь не работает, поскольку он исключает объекты из запроса вместо аннотирования их 0.

Обновление. Django 1.8 имеет новый conditional expressions feature, так что теперь мы можем сделать так:

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() 
    ))) 

Update 2. Django 2.0 имеет новую функцию Conditional aggregation см the accepted answer ниже.

ответ

6

Conditional aggregation в Django 2.0 позволяет вам еще больше уменьшить количество ошибок, которые были в прошлом. Это также будет использовать логику Postgres 'filter, которая несколько быстрее, чем суммарная (я видел цифры, подобные 20-30%).

Во всяком случае, в вашем случае, мы смотрим на что-то просто:

events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True)) 
) 

Там есть отдельный раздел в документации о filtering on annotations. Это то же самое, что и условная агрегация, но больше похоже на мой пример выше. Так или иначе, это намного здоровее, чем гнарные подзапросы, которые я делал раньше.

+0

Это выглядит потрясающе! :) – rudyryk

+0

Кстати, нет такого примера по ссылке на документацию, показывается только «агрегатное» использование. Вы уже тестировали такие запросы? (У меня нет и я хочу верить! :) – rudyryk

+2

У меня есть. Они работают. Я на самом деле попал в странный патч, где старый (супер сложный) подзапрос прекратил работать после обновления до Django 2.0, и мне удалось заменить его супер-простым отфильтрованным счетом. В аннотациях есть более эффективный пример in-doc, поэтому я сейчас задержу это. – Oli

24

ОБНОВЛЕНИЕ

Подход суб-запрос, который я уже теперь поддерживается в 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`" 
}) 
+0

Спасибо, Тодор! Похоже, я нашел способ, не используя '.extra', поскольку я предпочитаю избегать SQL в Django :) Я обновлю вопрос. – rudyryk

+1

Добро пожаловать, кстати, я знаю об этом подходе, но до сих пор это было нерабочее решение, поэтому я не упоминал об этом. Однако я просто обнаружил, что он исправлен в «Django 1.8.2», поэтому я думаю, что вы с этой версией, и именно поэтому он работает на вас. Вы можете узнать больше об этом [здесь] (http: // stackoverflow.com/questions/29440374/django-annotate-and-count-how-to-filter-the-ones-to-include-in-count) и [здесь] (https://code.djangoproject.com/ticket/24766) – Todor

+0

Я получаю, что это дает None, когда это должно быть 0. Кто-нибудь еще получает это? – Splatmistro

70

Просто обнаружил, что Django 1.8 имеет новый conditional expressions feature, так что теперь мы можем сделать так:

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() 
    ))) 
+0

Является ли это подходящим решением, когда совпадающих предметов много? Скажем, я хочу подсчитывать события кликов, которые произошли на последней неделе. – SverkerSbrg

+0

Почему бы и нет? Я имею в виду, почему ваш случай отличается? В вышеуказанном случае любое количество платных участников может участвовать в мероприятии. – rudyryk

+0

Я думаю, что вопрос @SverkerSbrg спрашивает, является ли это неэффективным для больших множеств, а не будет ли это работать ... правильно? Самое главное знать, что он не делает этого в python, он создает предложение SQL case - см. Https://github.com/django/django/blob/master/django/db/models/expressions.py#L831 - так что это будет достаточно результативно, простой пример будет лучше, чем объединение, но более сложные версии могут включать в себя подзапросы и т. д. –

1

Я хотел бы предложить используйте метод .values вашего запроса Participant.

Для краткости, что вы хотите сделать, это определяется по формуле:

Participant.objects\ 
    .filter(is_paid=True)\ 
    .values('event')\ 
    .distinct()\ 
    .annotate(models.Count('id')) 

полный пример выглядит следующим образом:

  1. Создать 2 Event S:

    event1 = Event.objects.create(title='event1') 
    event2 = Event.objects.create(title='event2') 
    
  2. Add Participant s к ним:

    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)] 
    
  3. Group все Participant s по их 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 s сгруппированы по их элементу event. Обратите внимание, что эти ведра содержат Participant.

  4. Вы можете затем аннотировать эти ведра, поскольку они содержат набор оригинальных Participant. Здесь мы хотим, чтобы подсчитать количество Participant, это просто сделано путем подсчета id х элементов в этих ведрах (так как те Participant):

    Participant.objects\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]> 
    
  5. Наконец, вы хотите только 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 по методу выше.

Смежные вопросы