Come eseguire una query come GROUP BY in django?


333

Richiedo un modello:

Members.objects.all()

E ritorna:

Eric, Salesman, X-Shop
Freddie, Manager, X2-Shop
Teddy, Salesman, X2-Shop
Sean, Manager, X2-Shop

Quello che voglio è sapere il modo migliore di Django di inviare una group_byquery al mio database, come:

Members.objects.all().group_by('designation')

Il che non funziona, ovviamente. So che possiamo fare alcuni trucchi django/db/models/query.py, ma sono solo curioso di sapere come farlo senza patch.

Risposte:


484

Se si intende eseguire l'aggregazione, è possibile utilizzare le funzionalità di aggregazione dell'ORM :

from django.db.models import Count
Members.objects.values('designation').annotate(dcount=Count('designation'))

Ciò si traduce in una query simile a

SELECT designation, COUNT(designation) AS dcount
FROM members GROUP BY designation

e l'output sarebbe nella forma

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]

6
@Harry: puoi incatenarlo. Qualcosa del tipo:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli,

57
ho una domanda, questa query restituisce solo la designazione e il dcount, e se volessi ottenere anche altri valori della tabella?
AJ

19
Se l'ordinamento è un campo diverso dalla designazione, non funzionerà senza reimpostare l'ordinamento. Vedere stackoverflow.com/a/1341667/202137
Gidgidonihah

12
@Gidgidonihah Vero, l'esempio dovrebbe leggereMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix,

7
ho una domanda, questa query restituisce solo la designazione e il dcount, e se volessi ottenere anche altri valori della tabella?
Yann

55

Una soluzione semplice, ma non il modo corretto è utilizzare SQL raw :

results = Members.objects.raw('SELECT * FROM myapp_members GROUP BY designation')

Un'altra soluzione è utilizzare la group_byproprietà:

query = Members.objects.all().query
query.group_by = ['designation']
results = QuerySet(query=query, model=Members)

Ora puoi iterare sulla variabile dei risultati per recuperare i risultati. Nota che group_bynon è documentato e potrebbe essere cambiato nella versione futura di Django.

E ... perché vuoi usare group_by? Se non si utilizza l'aggregazione, è possibile utilizzare order_byper ottenere un risultato simile.


Potete per favore dirmi come farlo usando order_by ??
Simplyharsh,

2
Salve, se non si utilizza l'aggregazione, è possibile emulare group_by utilizzando un order_by ed eliminare le voci non necessarie. Naturalmente, questa è un'emulazione ed è utilizzabile solo quando non si usano molti dati. Dato che non parlava di aggregazione, ho pensato che potesse essere una soluzione.
Michael,

Ehi, questo è fantastico - puoi per favore spiegare come usare execute_sql non sembra funzionare ..
rh0dium

8
Nota che questo non funziona più su Django 1.9. stackoverflow.com/questions/35558120/...
grokpot

1
Questo è un tipo di hack per usare l'ORM. Non dovresti istanziare nuovi queryset passando quelli vecchi manualmente.
Ian Kirkpatrick,

32

Puoi anche usare il regrouptag template per raggruppare per attributi. Dai documenti:

cities = [
    {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
    {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
    {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
    {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
    {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

...

{% regroup cities by country as country_list %}

<ul>
    {% for country in country_list %}
        <li>{{ country.grouper }}
            <ul>
            {% for city in country.list %}
                <li>{{ city.name }}: {{ city.population }}</li>
            {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

Somiglia a questo:

  • India
    • Mumbai: 19.000.000
    • Calcutta: 15.000.000
  • Stati Uniti d'America
    • New York: 20.000.000
    • Chicago: 7.000.000
  • Giappone
    • Tokyo: 33.000.000

Funziona anche su QuerySets credo.

fonte: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#regroup

modifica: nota che il regrouptag non funziona come ti aspetteresti se il tuo elenco di dizionari non è ordinato per chiave. Funziona in modo iterativo. Quindi ordina l'elenco (o il set di query) in base alla chiave del raggruppatore prima di passarlo al regrouptag.


1
Questo è perfetto! Ho cercato molto un modo semplice per farlo. E funziona anche su queryset, è così che l'ho usato.
CarmenA

1
questo è totalmente sbagliato se leggi dal database un grande set di dati e poi usi solo valori aggregati.
Sławomir Lenart,

@SławomirLenart certo, questo potrebbe non essere efficiente come una query DB diretta. Ma per casi d'uso semplici può essere una buona soluzione
inostia

Questo funzionerà se il risultato mostrato nel modello. Ma, per JsonResponse o altra risposta indiretta. questa soluzione non funzionerà.
Willy satrio nugroho,

1
@Willysatrionugroho se volessi farlo in una vista, ad esempio, stackoverflow.com/questions/477820/… potrebbe funzionare per te
inostia

7

Devi eseguire l'SQL personalizzato come esemplificato in questo frammento:

SQL personalizzato tramite subquery

O in un gestore personalizzato come mostrato nei documenti Django online:

Aggiunta di metodi Manager aggiuntivi


1
Tipo di soluzione di andata e ritorno. Lo avrei usato, se avessi avuto un uso prolungato di quello. Ma qui ho solo bisogno del numero di membri per designazione, tutto qui.
Semplicemente il

Nessun problema. Ho pensato di menzionare le funzionalità di aggregazione 1.1, ma ho ipotizzato che stavi usando la versione di rilascio :)
Van Gale

Si tratta solo di utilizzare query non elaborate, che mostrano la debolezza dell'ORM di Django.
Sławomir Lenart,

5

Django non supporta il gruppo gratuito per query . L'ho imparato nel modo peggiore. ORM non è progettato per supportare cose come ciò che si desidera fare, senza utilizzare SQL personalizzato. Sei limitato a:

  • Sql RAW (ovvero MyModel.objects.raw ())
  • cr.execute frasi (e un'analisi fatta a mano del risultato).
  • .annotate() (il gruppo per frasi viene eseguito nel modello figlio per .annotate (), in esempi come l'aggregazione di lines_count = Count ('lines'))).

Su un queryset qsè possibile chiamare qs.query.group_by = ['field1', 'field2', ...]ma è rischioso se non si conosce quale query si sta modificando e non si ha alcuna garanzia che funzioni e non si rompa l'interno dell'oggetto QuerySet. Inoltre, è un'API interna (non documentata) a cui non dovresti accedere direttamente senza rischiare che il codice non sia più compatibile con le future versioni di Django.


in effetti sei limitato non solo nel gruppo gratuito, quindi prova SQLAlchemy invece di Django ORM.
Sławomir Lenart,

5

C'è un modulo che ti consente di raggruppare i modelli Django e di lavorare ancora con un QuerySet nel risultato: https://github.com/kako-nawao/django-group-by

Per esempio:

from django_group_by import GroupByMixin

class BookQuerySet(QuerySet, GroupByMixin):
    pass

class Book(Model):
    title = TextField(...)
    author = ForeignKey(User, ...)
    shop = ForeignKey(Shop, ...)
    price = DecimalField(...)

class GroupedBookListView(PaginationMixin, ListView):
    template_name = 'book/books.html'
    model = Book
    paginate_by = 100

    def get_queryset(self):
        return Book.objects.group_by('title', 'author').annotate(
            shop_count=Count('shop'), price_avg=Avg('price')).order_by(
            'name', 'author').distinct()

    def get_context_data(self, **kwargs):
        return super().get_context_data(total_count=self.get_queryset().count(), **kwargs)

'Libro / books.html'

<ul>
{% for book in object_list %}
    <li>
        <h2>{{ book.title }}</td>
        <p>{{ book.author.last_name }}, {{ book.author.first_name }}</p>
        <p>{{ book.shop_count }}</p>
        <p>{{ book.price_avg }}</p>
    </li>
{% endfor %}
</ul>

La differenza con le query annotate/ aggregatebasic Django è l'uso degli attributi di un campo correlato, ad es book.author.last_name.

Se sono necessari i PK delle istanze che sono state raggruppate insieme, aggiungere la seguente annotazione:

.annotate(pks=ArrayAgg('id'))

NOTA: ArrayAggè una funzione specifica di Postgres, disponibile da Django 1.9 in poi: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg


Questo gruppo di django è un'alternativa al valuesmetodo. Penso che sia per scopi diversi.
LShi,

1
@LShi Non è un'alternativa ai valori, ovviamente no. valuesè un SQL selectmentre group_byè un SQL group by(come indica il nome ...). Perché il downvote? Stiamo usando tale codice in produzione per implementare group_bydichiarazioni complesse .
Risadinha,

Il suo documento dice group_by"si comporta principalmente come il metodo dei valori, ma con una differenza ..." Il documento non menziona SQL GROUP BYe il caso d'uso che fornisce non suggerisce che abbia nulla a che fare con SQL GROUP BY. Ritirerò il voto negativo quando qualcuno lo ha chiarito, ma quel documento è davvero fuorviante.
LShi,

Dopo aver letto il documento pervalues , ho scoperto che mi mancava che valuesfunziona come un GROUP BY. È colpa mia. Penso che sia più semplice da usare itertools.groupbyrispetto a questo gruppo di django quando valuesè insufficiente.
LShi,

1
È impossibile fare quanto group bysopra con una semplice valueschiamata -con o senza annotatee senza recuperare tutto dal database. Il tuo suggerimento di itertools.groupbylavorare per piccoli set di dati ma non per diverse migliaia di set di dati che probabilmente vuoi pagina. Naturalmente, a quel punto dovrai pensare a un indice di ricerca speciale che contenga comunque dati preparati (già raggruppati).
Risadinha,

0

Il documento afferma che è possibile utilizzare i valori per raggruppare il set di query.

class Travel(models.Model):
    interest = models.ForeignKey(Interest)
    user = models.ForeignKey(User)
    time = models.DateTimeField(auto_now_add=True)

# Find the travel and group by the interest:

>>> Travel.objects.values('interest').annotate(Count('user'))
<QuerySet [{'interest': 5, 'user__count': 2}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited for 2 times, 
# and the interest(id=6) had only been visited for 1 time.

>>> Travel.objects.values('interest').annotate(Count('user', distinct=True)) 
<QuerySet [{'interest': 5, 'user__count': 1}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited by only one person (but this person had 
#  visited the interest for 2 times

Puoi trovare tutti i libri e raggrupparli per nome usando questo codice:

Book.objects.values('name').annotate(Count('id')).order_by() # ensure you add the order_by()

Puoi guardare un po 'di foglio qui .


-1

Se non sbaglio, puoi usare qualunque cosa-query-set .group_by = [' field ']


8
Questo non è il caso, almeno in Django 1.6: l'oggetto 'QuerySet' non ha alcun attributo 'group_by'
Facundo Olano,

1
Un uso corretto potrebbe essere queryset.query.group_by = [...] ma ciò spezzerebbe la semantica della query e non funzionerebbe come previsto.
Luis Masuelli,

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.