Come filtrare gli oggetti per l'annotazione del conteggio in Django?


123

Considera semplici modelli Django Evente 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)

È facile annotare la query sugli eventi con il numero totale di partecipanti:

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

Come annotare con il conteggio dei partecipanti filtrati per is_paid=True ?

Ho bisogno di interrogare tutti gli eventi indipendentemente dal numero di partecipanti, ad esempio non ho bisogno di filtrare per risultato annotato. Se ci sono0 partecipanti, va bene, ho solo bisogno 0di un valore annotato.

L' esempio della documentazione non funziona qui, perché esclude gli oggetti dalla query invece di annotarli con 0.

Aggiornare. Django 1.8 ha una nuova funzionalità per le espressioni condizionali , quindi ora possiamo fare in questo modo:

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

Aggiornamento 2. Django 2.0 ha una nuova funzionalità di aggregazione condizionale , vedere la risposta accettata di seguito.

Risposte:


105

L'aggregazione condizionale in Django 2.0 ti consente di ridurre ulteriormente la quantità di faff che è stata in passato. Questo utilizzerà anche la filterlogica di Postgres , che è un po 'più veloce di un sum-case (ho visto numeri come il 20-30% banditi).

Ad ogni modo, nel tuo caso, stiamo guardando qualcosa di semplice come:

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

C'è una sezione separata nei documenti sul filtraggio delle annotazioni . È la stessa cosa dell'aggregazione condizionale ma più simile al mio esempio sopra. In ogni caso, questo è molto più sano delle sottoquery nodose che stavo facendo prima.


A proposito, non esiste un tale esempio dal collegamento alla documentazione, aggregateviene mostrato solo l' utilizzo. Hai già testato queste domande? (Non l'ho fatto e voglio crederci! :)
Rudyryk

2
Io ho. Lavorano. In realtà ho trovato una strana patch in cui una vecchia sottoquery (super complicata) ha smesso di funzionare dopo l'aggiornamento a Django 2.0 e sono riuscito a sostituirla con un conteggio filtrato semplicissimo. Esiste un esempio in-doc migliore per le annotazioni, quindi lo inserirò ora.
Oli

1
Ci sono alcune risposte qui, questo è il modo Django 2.0, e di seguito troverai il modo Django 1.11 (sottoquery) e il modo Django 1.8.
Ryan Castner

2
Attenzione, se lo provi in ​​Django <2, ad esempio 1.9, verrà eseguito senza eccezioni, ma il filtro semplicemente non viene applicato. Quindi potrebbe sembrare che funzioni con Django <2, ma non lo fa.
djvg

Se è necessario aggiungere più filtri, è possibile aggiungerli nell'argomento Q () separati da, ad esempio filtro = Q (partecipanti__is_paid = True, qualcos'altro = valore)
Tobit

93

Ho appena scoperto che Django 1.8 ha una nuova funzionalità di espressioni condizionali , quindi ora possiamo fare in questo modo:

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

È una soluzione idonea quando gli articoli corrispondenti sono molti? Diciamo che voglio contare gli eventi di clic verificatisi nell'ultima settimana.
SverkerSbrg

Perchè no? Voglio dire, perché il tuo caso è diverso? Nel caso precedente ci può essere un numero qualsiasi di partecipanti pagati all'evento.
Rudyryk

Penso che la domanda che @SverkerSbrg si sta ponendo sia se questo è inefficiente per set di grandi dimensioni, piuttosto che se funzionerebbe o meno .... corretto? La cosa più importante da sapere è che non lo sta facendo in Python, ma sta creando una clausola case SQL - vedi github.com/django/django/blob/master/django/db/models/… - quindi sarà ragionevolmente performante, un semplice esempio sarebbe migliore di un join, ma versioni più complesse potrebbero includere sottoquery ecc.
Hayden Crocker

1
Quando si usa questo con Count(invece di Sum) immagino che dovremmo impostare default=None(se non si usa l' filterargomento django 2 ).
djvg

41

AGGIORNARE

L'approccio di sottoquery che ho citato è ora supportato in Django 1.11 tramite espressioni di sottoquery .

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

Lo preferisco all'aggregazione (somma + maiuscole / minuscole) , perché dovrebbe essere più veloce e più facile da ottimizzare (con una corretta indicizzazione) .

Per la versione precedente, lo stesso può essere ottenuto utilizzando .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`"
})

Grazie Todor! Sembra che io abbia trovato la strada senza usare .extra, dato che preferisco evitare SQL in Django :) Aggiornerò la domanda.
Rudyryk

1
Sei il benvenuto, ma sono consapevole di questo approccio, ma fino ad ora era una soluzione non funzionante, ecco perché non ne ho parlato. Tuttavia ho appena scoperto che è stato risolto Django 1.8.2, quindi immagino che tu sia con quella versione ed è per questo che funziona per te. Puoi leggere di più su questo qui e qui
Todor

2
Ho capito che questo produce un Nessuno quando dovrebbe essere 0. Qualcun altro lo sta ottenendo?
StefanJCollier

@StefanJCollier Sì, ce l'ho Noneanch'io. La mia soluzione era usare Coalesce( from django.db.models.functions import Coalesce). Si utilizza in questo modo: Coalesce(Subquery(...), 0). Potrebbe esserci un approccio migliore, però.
Adam Taylor

6

Suggerirei di utilizzare il .valuesmetodo del tuoParticipant set query.

In breve, quello che vuoi fare è dato da:

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

Un esempio completo è il seguente:

  1. Crea 2 Events:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
  2. Aggiungi Participants a loro:

    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. Raggruppa tutti Participanti messaggi in base al loro eventcampo:

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

    Qui è necessario distinto:

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

    Quello .valuesche .distinctstanno facendo qui è che stanno creando due secchi di messaggi Participantraggruppati in base al loro elemento event. Nota che quei secchi contengono Participant.

  4. È quindi possibile annotare quei bucket poiché contengono il set di originali Participant. Qui vogliamo contare il numero di Participant, questo viene semplicemente fatto contando le ids degli elementi in quei bucket (poiché quelli sono Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
  5. Infine vuoi solo Participantcon un is_paidessere True, puoi semplicemente aggiungere un filtro davanti all'espressione precedente, e questo produce l'espressione mostrata sopra:

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

L'unico inconveniente è che devi recuperare il Eventdopo poiché hai solo idil metodo sopra.


2

Quale risultato sto cercando:

  • Persone (assegnatario) a cui sono state aggiunte attività a un report. - Conteggio unico totale di persone
  • Persone che hanno attività aggiunte a un report ma, solo per attività la cui fatturabilità è maggiore di 0.

In generale, dovrei utilizzare due query diverse:

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

Ma voglio entrambi in una query. Quindi:

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

Risultato:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.