2015-06-10 11 views
47

consideri semplici modelli Django Event e Participant:Come filtrare gli oggetti per l'annotazione del conteggio 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) 

E 'facile per annotare eventi di query con il numero totale dei partecipanti:

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

Come annotare con conteggio dei partecipanti filtrata da is_paid=True?

Ho bisogno di interrogare tutti gli eventi indipendentemente dal numero di partecipanti, ad es. Non ho bisogno di filtrare per risultato annotato. Se ci sono 0 partecipanti, va bene, ho solo bisogno di 0 nel valore annotato.

Il example from documentation non funziona qui, perché esclude gli oggetti dalla query invece di annotarli con 0.

Aggiornamento . Django 1.8 dispone di nuove conditional expressions feature, così 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() 
    ))) 

Update 2. Django 2.0 ha una nuova funzione di Conditional aggregation, vedi the accepted answer sotto.

risposta

6

Conditional aggregation in Django 2.0 consente di ridurre ulteriormente la quantità di file in uscita nel passato. Questo userà anche la logica di Postgres filter, che è un po 'più veloce di un caso limite (ho visto numeri come il 20-30% delimitati).

In ogni caso, nel tuo caso, stiamo cercando qualcosa di semplice come:

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

C'è una sezione separata nella documentazione circa filtering on annotations. È la stessa roba dell'aggregazione condizionale, ma più simile al mio esempio sopra. In entrambi i casi, questo è molto più sano delle subquery nodose che stavo facendo prima.

+0

Questo è fantastico! :) – rudyryk

+0

BTW, non esiste un tale esempio dal collegamento della documentazione, viene mostrato solo l'utilizzo "aggregato". Hai già provato queste domande? (Non ho e voglio crederci! :) – rudyryk

+2

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

24

UPDATE

L'approccio sub-query che cito è ora supportata in Django 1.11 via 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() 
    ) 
) 

Preferisco questo oltre aggregazione (somma + caso), perché dovrebbe essere più veloce e più facile da essere ottimizzati (con una corretta indicizzazione).

Per la versione più vecchia, lo stesso può essere raggiunto 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`" 
}) 
+0

Grazie Todor! Sembra che abbia trovato la strada senza usare '.extra', come preferisco evitare SQL in Django :) aggiornerò la domanda. – rudyryk

+1

Siete i benvenuti, btw sono a conoscenza di questo approccio, ma fino ad ora era una soluzione non funzionante, ecco perché non ne ho parlato. Tuttavia ho appena scoperto che è stato corretto in 'Django 1.8.2', quindi credo che tu sia con quella versione ed è per questo che funziona per te. Puoi leggere di più a riguardo [qui] (http: // StackOverflow.it/questions/29440374/django-annotate-and-count-how-to-filter-the-ones-to-include-in-count) e [qui] (https://code.djangoproject.com/ticket/24766) – Todor

+0

Ho capito che questo produce un None quando dovrebbe essere 0. Chiunque altro lo ottiene? – Splatmistro

70

appena scoperto che Django 1.8 ha un nuovo conditional expressions feature, così 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() 
    ))) 
+0

Questa è una soluzione idonea quando gli articoli corrispondenti sono molti? Diciamo che voglio contare gli eventi click che si sono verificati nell'ultima settimana. – SverkerSbrg

+0

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

+0

Penso che la domanda che @SverkerSbrg sta chiedendo è se questo è inefficace per insiemi di grandi dimensioni, piuttosto che se non funzionerebbe ... corretto? La cosa più importante da sapere è che non lo sta eseguendo in Python, sta creando una clausola SQL - vedi https://github.com/django/django/blob/master/django/db/models/expressions.py#L831 - quindi sarà ragionevolmente performante, un semplice esempio sarebbe meglio di un join, ma versioni più complesse potrebbero includere subquery ecc. –

1

Vorrei suggerire a utilizzare invece il metodo .values del proprio queryset Participant.

in breve, ciò che si vuole fare è data da:

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

Un esempio completo è il seguente:

  1. creare 2 Event s:

    event1 = Event.objects.create(title='event1') 
    event2 = Event.objects.create(title='event2') 
    
  2. Aggiungi Participant s 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. Gruppo tutti Participant s per la loro event campo:

    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 è necessaria distinti:

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

    Cosa .values e .distinct stanno facendo qui è che stanno creando due secchi di Participant s raggruppati per il loro elemento event. Si noti che questi bucket contengono Participant.

  4. È quindi possibile annotare i bucket in quanto contengono il set dell'originale Participant. Qui vogliamo contare il numero di Participant, questo è semplicemente fatto contando i id s degli elementi a quelli secchi (in quanto tali sono Participant):

    Participant.objects\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]> 
    
  5. Infine desideri solo Participant con un is_paid essere True , si può solo aggiungere un filtro di fronte alla precedente espressione, e questo cedere l'espressione sopra indicato:

    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 è th devi recuperare lo Event in seguito, dato che hai il solo id dal metodo sopra.

Problemi correlati