2015-06-10 5 views
47

Betrachten einfache Django Modelle Event und Participant:Wie filtert man Objekte für Count Annotation in 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) 

Es ist einfach Ereignisse Abfrage mit Gesamtzahl der Teilnehmer mit Anmerkungen versehen:

events = Event.objects.all().annotate(participants=models.Count('participant')) 

Wie mit Anmerkungen versehen mit der Zählung der Teilnehmer durch is_paid=True gefiltert?

Ich muss alle Ereignisse unabhängig von der Anzahl der Teilnehmer, z. Ich muss nicht nach annotierten Ergebnissen filtern. Wenn es 0 Teilnehmer gibt, ist das in Ordnung, ich brauche nur 0 in annotierten Wert.

Die example from documentation funktioniert hier nicht, da sie Objekte von Abfrage ausschließt, anstatt sie mit 0 zu kommentieren.

Aktualisierung. Django 1.8 hat neue conditional expressions feature, so jetzt wir so tun können:

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 neue Conditional aggregation Funktion hat, siehe the accepted answer unten.

Antwort

6

Conditional aggregation in Django 2.0 können Sie die Menge an Faff, die in der Vergangenheit war, weiter reduzieren. Dies wird auch die filter Logik von Postgres verwenden, die etwas schneller ist als eine Summe (ich habe Zahlen gesehen, die zwischen 20 und 30% liegen).

Wie auch immer, in Ihrem Fall, wir suchen auf etwas so einfach wie:

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

Es gibt einen separaten Abschnitt in der Dokumentation über filtering on annotations. Es ist das gleiche wie bedingte Aggregation, aber eher wie in meinem obigen Beispiel. Entweder ist das viel gesünder als die gnarly Unterabfragen, die ich vorher tat.

+0

Das sieht großartig aus! :) – rudyryk

+0

BTW, es gibt kein solches Beispiel durch den Dokumentationslink, nur "aggregierte" Verwendung wird angezeigt. Haben Sie solche Anfragen bereits getestet? (Ich habe nicht und ich will es glauben! :) – rudyryk

+2

Ich habe. Sie arbeiten. Ich traf tatsächlich einen seltsamen Patch, wo nach dem Upgrade auf Django 2.0 eine alte (super-komplizierte) Unterabfrage nicht mehr funktionierte und ich es durch eine sehr einfache gefilterte Zählung ersetzen konnte. Es gibt ein besseres in-doc-Beispiel für Anmerkungen, also werde ich das jetzt machen. – Oli

24

UPDATE

Die Unterabfrage Ansatz, die ich schon erwähnt, ist jetzt in Django 1.11 über subquery-expressions unterstützt.

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

Ich ziehe diese über die Aggregation (Summe + case), weil es schneller sein sollte und leichter zu optimiert werden (mit Indexierung).

Für ältere Version kann die gleiche .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

Danke Todor! Scheint so, als hätte ich den Weg gefunden, ohne '.extra' zu verwenden, da ich es vorziehe, SQL in Django zu vermeiden :) Ich werde die Frage aktualisieren. – rudyryk

+1

Gern geschehen, bin ich mir dieser Vorgehensweise bewusst, aber es war bis jetzt eine nicht funktionierende Lösung, deshalb habe ich nichts darüber gesagt. Ich habe jedoch festgestellt, dass es in 'Django 1.8.2' behoben wurde, also denke ich, dass du bei dieser Version bist und deshalb funktioniert es für dich. Sie können mehr darüber [hier] lesen (http: // stackoverflow.com/questions/29440374/django-kommentieren-und-zählen-how-to-filter-the-one-to-include-in-count) und [hier] (https://code.djangoproject.com/ticket/24766) – Todor

+0

Ich bekomme, dass dies eine None erzeugt, wenn es 0 sein sollte. Wer sonst bekommt das? – Splatmistro

70

nur entdeckt werden unter Verwendung erreicht, dass Django 1.8 hat neue conditional expressions feature, so können wir jetzt tun, wie folgt:

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

Ist dies eine geeignete Lösung, wenn die passenden Elemente viele sind? Lassen Sie uns sagen, dass ich Klickereignisse zählen möchte, die in der letzten Woche aufgetreten sind. – SverkerSbrg

+0

Warum nicht? Ich meine, warum ist dein Fall anders? Im oben genannten Fall kann eine beliebige Anzahl von bezahlten Teilnehmern am Event teilnehmen. – rudyryk

+0

Ich denke, die Frage, die @SverkerSbrg stellt, lautet, ob dies für große Mengen ineffizient ist, und nicht ob es funktionieren würde oder nicht .... richtig? Das Wichtigste ist, dass es in Python nicht funktioniert. Es wird eine SQL-Fallklausel erstellt - siehe https://github.com/django/django/blob/master/django/db/models/expressions.py#L831 - so wird es einigermaßen performant, einfaches Beispiel wäre besser als ein Join, aber komplexere Versionen könnten Unterabfragen etc. enthalten. –

1

Ich würde vorschlagen, zu Verwenden Sie stattdessen die .values-Methode Ihres -Abfrage-Sets.

kurz, was Sie tun möchten, ist gegeben durch:

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

Ein vollständiges Beispiel ist wie folgt:

  1. erstellen 2 Event s:

    event1 = Event.objects.create(title='event1') 
    event2 = Event.objects.create(title='event2') 
    
  2. Fügen Sie zu ihnen hinzu:

    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. Gruppe alle Participant s durch ihre event Feld:

    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)...']> 
    

    hier deutliche gebraucht wird:

    Participant.objects.values('event').distinct() 
    > <QuerySet [{'event': 1}, {'event': 2}]> 
    

    Was für .values und .distinct tun hier ist, dass sie die Schaffung von zwei Eimer s gruppiert nach ihrem Element event. Beachten Sie, dass diese Buckets enthalten.

  4. Sie können diese Buckets mit Anmerkungen versehen, da sie den Satz der ursprünglichen enthalten. Hier haben wir die Anzahl der Participant zählen möchten, ist dies einfach durch Zählen der id s der Elemente in den Eimer getan (da diese Participant sind):

    Participant.objects\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]> 
    
  5. Schließlich wollen Sie nur Participant mit einem is_paidTrue sein

    Participant.objects\ 
        .filter(is_paid=True)\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]> 
    

der einzige Nachteil ist th:, Sie können nur einen Filter vor dem vorherigen Ausdruck hinzuzufügen, und das ist der Ausdruck oben gezeigt ergeben bei Sie müssen die Event danach abrufen, wie Sie nur die id von der obigen Methode haben.

Verwandte Themen