Wie filtere ich Objekte für Zählanmerkungen in Django?

123

Betrachten Sie einfache Django-Modelle Eventund 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, Ereignisabfragen mit der Gesamtzahl der Teilnehmer zu kommentieren:

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

Wie kommentiere ich mit der Anzahl der Teilnehmer, nach denen gefiltert wurde is_paid=True?

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

Das Beispiel aus der Dokumentation funktioniert hier nicht, da Objekte von der Abfrage ausgeschlossen werden, anstatt sie mit Anmerkungen zu versehen 0.

Aktualisieren. Django 1.8 verfügt über eine neue Funktion für bedingte Ausdrücke. Jetzt können wir Folgendes tun:

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 verfügt über eine neue Funktion zur bedingten Aggregation (siehe die akzeptierte Antwort unten).

Rudyryk
quelle

Antworten:

104

Durch die bedingte Aggregation in Django 2.0 können Sie die Menge an Faff, die dies in der Vergangenheit war, weiter reduzieren. Dies wird auch die filterLogik von Postgres verwenden, die etwas schneller ist als ein Summenfall (ich habe Zahlen wie 20-30% gesehen, die herumgespielt wurden).

Wie auch immer, in Ihrem Fall betrachten wir 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 zum Filtern nach Anmerkungen . Es ist das gleiche Zeug wie die bedingte Aggregation, aber eher wie mein Beispiel oben. In beiden Fällen ist dies viel gesünder als die knorrigen Unterabfragen, die ich zuvor durchgeführt habe.

Oli
quelle
Übrigens gibt es kein solches Beispiel über den Dokumentationslink, nur die aggregateVerwendung wird angezeigt. Haben Sie solche Abfragen bereits getestet? (Ich habe nicht und ich möchte glauben! :)
Rudyryk
2
Ich habe. Sie arbeiten. Ich habe tatsächlich einen seltsamen Patch gefunden, bei dem eine alte (super komplizierte) Unterabfrage nach dem Upgrade auf Django 2.0 nicht mehr funktioniert, und ich habe es geschafft, sie durch eine supereinfache gefilterte Anzahl zu ersetzen. Es gibt ein besseres In-Doc-Beispiel für Anmerkungen, daher werde ich das jetzt erläutern.
Oli
1
Hier gibt es einige Antworten, dies ist der Django 2.0-Weg, und unten finden Sie den Django 1.11-Weg (Unterabfragen) und den Django 1.8-Weg.
Ryan Castner
2
Passen Sie auf , wenn Sie versuchen , diese in Django <2, zB 1,9, es wird ausnahmslos laufen, aber die Filter einfach nicht angewandt wird . Es scheint also mit Django <2 zu funktionieren, tut es aber nicht.
DJVG
Wenn Sie mehrere Filter hinzufügen müssen, können Sie diese im Argument Q () hinzufügen, wobei beispielsweise filter = Q (Teilnehmer__is_paid = True, etwas
Tobit getrennt ist
93

Wir haben gerade festgestellt, dass Django 1.8 über eine neue Funktion für bedingte Ausdrücke verfügt. Jetzt können wir Folgendes tun:

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()
    )))
Rudyryk
quelle
Ist dies eine geeignete Lösung, wenn viele übereinstimmende Elemente vorhanden sind? Nehmen wir an, ich möchte Klickereignisse zählen, die in der letzten Woche aufgetreten sind.
SverkerSbrg
Warum nicht? Ich meine, warum ist Ihr Fall anders? Im obigen Fall kann eine beliebige Anzahl von bezahlten Teilnehmern an der Veranstaltung teilnehmen.
Rudyryk
Ich denke, die Frage, die @SverkerSbrg stellt, ist, ob dies für große Mengen ineffizient ist und nicht, ob es funktionieren würde oder nicht ... richtig? Das Wichtigste zu wissen ist, dass es nicht in Python ausgeführt wird, sondern eine SQL-Case-Klausel erstellt - siehe github.com/django/django/blob/master/django/db/models/… -, damit es einigermaßen leistungsfähig ist. Ein einfaches Beispiel wäre besser als ein Join, aber komplexere Versionen könnten Unterabfragen usw. enthalten.
Hayden Crocker
1
Wenn wir dies mit Count(anstelle von Sum) verwenden, sollten wir es setzen default=None(wenn nicht das filterArgument django 2 verwendet wird ).
DJVG
41

AKTUALISIEREN

Der von mir erwähnte Unterabfrage- Ansatz wird jetzt in Django 1.11 über Unterabfrage-Ausdrü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 zu optimieren sein sollte (bei richtiger Indizierung) .

Für ältere Versionen kann das gleiche mit erreicht werden .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`"
})
Todor
quelle
Danke Todor! Scheint .extra, als hätte ich den Weg ohne Verwendung gefunden , da ich SQL in Django lieber meide :) Ich werde die Frage aktualisieren.
Rudyryk
1
Sie sind willkommen, übrigens, ich bin mir dieses Ansatzes bewusst, aber es war bis jetzt eine nicht funktionierende Lösung, deshalb habe ich es nicht erwähnt. Allerdings habe ich gerade festgestellt, dass es behoben wurde Django 1.8.2, also denke ich, dass Sie mit dieser Version sind und deshalb funktioniert es für Sie. Sie können hier und hier
Todor
2
Ich verstehe, dass dies ein None erzeugt, wenn es 0 sein sollte. Bekommt sonst noch jemand das?
StefanJCollier
@StefanJCollier Ja, ich habe Noneauch. Meine Lösung war Coalesce( from django.db.models.functions import Coalesce) zu verwenden. Sie verwenden es so : Coalesce(Subquery(...), 0). Es kann jedoch einen besseren Ansatz geben.
Adam Taylor
6

Ich würde vorschlagen, stattdessen die .valuesMethode Ihres ParticipantAbfragesets zu verwenden.

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. Erstellen Sie 2 Events:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
  2. Fügen Sie Participantihnen s 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. Gruppieren Sie alle Participants nach ihrem eventFeld:

    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 ist etwas Besonderes erforderlich:

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

    Was .valuesund .distincttun hier ist, dass sie zwei Eimer von Participants erstellen, die nach ihrem Element gruppiert sind event. Beachten Sie, dass diese Eimer enthalten Participant.

  4. Sie können diese Buckets dann mit Anmerkungen versehen, da sie den Originalsatz enthalten Participant. Hier wollen wir die Anzahl zählen Participant, dies geschieht einfach durch Zählen der ids der Elemente in diesen Buckets (da diese sind Participant):

    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 Participantmit einem is_paidWesen True, Sie können einfach einen Filter vor dem vorherigen Ausdruck hinzufügen, und 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 das Eventspäter abrufen müssen, da Sie nur das idvon der obigen Methode haben.

Raffi
quelle
2

Welches Ergebnis suche ich:

  • Personen (Beauftragter), denen einem Bericht Aufgaben hinzugefügt wurden. - Gesamtzahl der Personen
  • Personen, denen Aufgaben zu einem Bericht hinzugefügt wurden, für Aufgaben, deren Abrechnungsfähigkeit nur mehr als 0 beträgt.

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}]>
Arindam Roychowdhury
quelle