it-swarm.com.de

Wie filtere ich Objekte in Django nach Count Annotation?

Betrachten Sie einfach Django Modelle Event und 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)

Es ist einfach, die Ereignisabfrage mit der Gesamtzahl der Teilnehmer zu kommentieren:

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

Wie kommentiere ich mit der Anzahl der nach is_paid=True Gefilterten Teilnehmer?

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

Das Beispiel aus der Dokumentation funktioniert hier nicht, weil es Objekte von der Abfrage ausschließt, anstatt sie mit 0 Zu kommentieren.

Update. Django 1.8 hat eine neue Funktion Bedingte Ausdrücke , also können wir jetzt mache das so:

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 hat eine neue Bedingte Aggregation Funktion, siehe die akzeptierte Antwort unten.

103
rudyryk

Bedingte Aggregation in Django= 2.0 ermöglicht es Ihnen, die Menge an Faff weiter zu reduzieren, die dies in der Vergangenheit war. Dies wird auch die filter -Logik von Postgres verwenden , was etwas schneller ist als ein Summenfall (ich habe Zahlen wie 20-30% gesehen).

Auf jeden Fall betrachten wir in Ihrem Fall etwas so Einfaches wie:

from Django.db.models import Q, Count
events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)

In den Dokumenten gibt es einen separaten Abschnitt über Filtern nach Anmerkungen . Es ist das gleiche wie bei der bedingten Aggregation, entspricht aber eher meinem obigen Beispiel. So oder so, das ist viel gesünder als die knorrigen Unterabfragen, die ich vorher gemacht habe.

63
Oli

Gerade entdeckt, dass Django 1.8 neue bedingte Ausdrücke enthält , also können wir das jetzt so machen:

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

[~ # ~] Update [~ # ~]

Der von mir erwähnte Unterabfrageansatz wird jetzt in Django 1.11 über nterabfrageausdrücke 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 bevorzuge dies gegenüber der Aggregation (Summe + Fall), da es schneller und einfacher sein sollte, optimiert zu werden (mit korrekter Indizierung).

Für ältere Versionen kann dasselbe mit .extra erreicht werden

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`"
})
38
Todor

Ich würde vorschlagen, das .values Methode Ihres Participant Abfragesatzes.

Kurz gesagt, 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 lautet wie folgt:

  1. Erstelle 2 Events:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    
  2. Fügen Sie ihnen Participants 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. Gruppiere alle Participants nach ihrem 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 wird unterschieden:

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

    Was .values und .distinct tun hier, dass sie zwei Buckets von Participants erstellen, die nach ihrem Element event gruppiert sind. Beachten Sie, dass diese Buckets Participant enthalten.

  4. Sie können diese Buckets dann mit Anmerkungen versehen, da sie den Satz des ursprünglichen Participant enthalten. Hier möchten wir die Anzahl von Participant zählen. Dies geschieht einfach durch Zählen der ids der Elemente in diesen Buckets (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 möchten Sie nur Participant mit einem is_paid Da True ist, können Sie einfach einen Filter vor dem vorherigen Ausdruck einfügen. Dies ergibt den oben gezeigten Ausdruck:

    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, dass Sie danach Event abrufen müssen, da Sie nur id aus der obigen Methode haben.

4
Raffi

Welches Ergebnis suche ich:

  • Personen (Bevollmächtigte), denen Aufgaben zu einem Bericht hinzugefügt wurden. - Gesamtzahl der eindeutigen Personen
  • Personen, denen Aufgaben zu einem Bericht hinzugefügt wurden, für die jedoch nur eine Rechnung mit mehr als 0 möglich ist.

Im Allgemeinen müsste ich zwei verschiedene Abfragen verwenden:

Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()

Aber ich möchte beides in einer Abfrage. Daher:

Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))

Ergebnis:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>