Django-Äquivalent für Zählung und Gruppierung nach

91

Ich habe ein Modell, das so aussieht:

class Category(models.Model):
    name = models.CharField(max_length=60)

class Item(models.Model):
    name = models.CharField(max_length=60)
    category = models.ForeignKey(Category)

Ich möchte die Anzahl (nur die Anzahl) der Elemente für jede Kategorie auswählen, daher wäre dies in SQL so einfach:

select category_id, count(id) from item group by category_id

Gibt es ein Äquivalent zu diesem "Django-Weg"? Oder ist einfaches SQL die einzige Option? Ich bin mit der count () -Methode in Django vertraut , sehe jedoch nicht, wie group by dort passen würde.

Sergey Golovchenko
quelle
Mögliches Duplikat von Wie wird in Django als GROUP BY abgefragt?
Ciro Santilli 法轮功 冠状 病 六四 事件 25
@CiroSantilli is 文件 六四 事件 法轮功 wie ist das ein Duplikat? Diese Frage wurde 2008 gestellt und die Frage, auf die Sie sich beziehen, ist 2 Jahre später.
Sergey Golovchenko
Der derzeitige Konsens besteht darin, durch "Qualität" zu schließen: < meta.stackexchange.com/questions/147643/… > Da "Qualität" nicht messbar ist, gehe ich einfach durch Upvotes. ;-) Wahrscheinlich kommt es darauf an, welche Frage die besten Google-Neulinge im Titel trifft.
Ciro Santilli 法轮功 冠状 病 六四 事件 26

Antworten:

131

Wie ich gerade herausgefunden habe, geht hier Folgendes mit der Django 1.1-Aggregations-API vor:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Michael
quelle
3
Wie die meisten Dinge in Django ist nichts davon sinnvoll anzusehen, aber (im Gegensatz zu den meisten Dingen in Django), als ich es tatsächlich ausprobiert habe, war es fantastisch: P
jsh
3
Beachten Sie, dass Sie verwenden müssen, order_by()wenn dies 'category'nicht die Standardreihenfolge ist. (Siehe Daniels umfassendere Antwort.)
Rick Westera
Der Grund, warum dies funktioniert, liegt darin, dass es .annotate()nach a etwas anders funktioniert.values() : "Wenn jedoch eine values ​​() -Klausel verwendet wird, um die in der Ergebnismenge zurückgegebenen Spalten einzuschränken, unterscheidet sich die Methode zum Auswerten von Anmerkungen geringfügig. Anstatt eine Anmerkung zurückzugeben Ergebnis Für jedes Ergebnis im ursprünglichen QuerySet werden die ursprünglichen Ergebnisse gemäß den eindeutigen Kombinationen der in der values ​​() -Klausel angegebenen Felder gruppiert. "
mgalgs
58

( Update : Die vollständige Unterstützung der ORM-Aggregation ist jetzt in Django 1.1 enthalten . Gemäß der folgenden Warnung zur Verwendung privater APIs funktioniert die hier dokumentierte Methode in Post-1.1-Versionen von Django nicht mehr. Ich habe nicht nachgeforscht, um herauszufinden, warum; Wenn Sie 1.1 oder höher verwenden, sollten Sie trotzdem die echte Aggregations-API verwenden .)

Die Unterstützung für die Kernaggregation war bereits in Version 1.0 vorhanden. Es ist nur undokumentiert, nicht unterstützt und verfügt noch nicht über eine benutzerfreundliche API. Aber so können Sie es trotzdem verwenden, bis 1.1 eintrifft (auf eigenes Risiko und in vollem Wissen, dass das Attribut query.group_by nicht Teil einer öffentlichen API ist und sich ändern könnte):

query_set = Item.objects.extra(select={'count': 'count(1)'}, 
                               order_by=['-count']).values('count', 'category')
query_set.query.group_by = ['category_id']

Wenn Sie dann über query_set iterieren, ist jeder zurückgegebene Wert ein Wörterbuch mit einem "Kategorie" -Schlüssel und einem "Zähl" -Schlüssel.

Sie müssen hier nicht nach Anzahl bestellen, dies ist nur enthalten, um zu demonstrieren, wie es gemacht wird (es muss im Aufruf von .extra () gemacht werden, nicht anderswo in der Queryset-Konstruktionskette). Sie können auch genauso gut count (id) anstelle von count (1) sagen, aber letzteres ist möglicherweise effizienter.

Beachten Sie auch, dass beim Festlegen von .query.group_by die Werte tatsächliche DB-Spaltennamen ('category_id') und keine Django-Feldnamen ('category') sein müssen. Dies liegt daran, dass Sie die internen Abfragen auf einer Ebene optimieren, auf der alles in DB-Begriffen und nicht in Django-Begriffen angegeben ist.

Carl Meyer
quelle
+1 für die alte Methode. Auch wenn es derzeit nicht unterstützt wird, ist es gelinde gesagt aufschlussreich. Wirklich erstaunlich.
Luftangriff
Schauen Sie sich die Django-Aggregations-API unter docs.djangoproject.com/de/dev/topics/db/aggregation/ an. Andere komplexe Aufgaben können damit erledigt werden. Dort finden Sie einige leistungsstarke Beispiele.
Serfer2
@ serfer2 Ja, diese Dokumente sind bereits oben in dieser Antwort verlinkt.
Carl Meyer
56

Da ich ein wenig verwirrt darüber war, wie das Gruppieren in Django 1.1 funktioniert, dachte ich, ich würde hier näher erläutern, wie genau Sie es verwenden. Um zu wiederholen, was Michael gesagt hat:

Wie ich gerade herausgefunden habe, geht hier Folgendes mit der Django 1.1-Aggregations-API vor:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))

Beachten Sie auch, dass Sie müssen from django.db.models import Count!

Dadurch werden nur die Kategorien ausgewählt und anschließend eine aufgerufene Anmerkung hinzugefügt category__count. Abhängig von der Standardreihenfolge ist dies möglicherweise alles, wascategory Sie benötigen. Wenn die Standardreihenfolge jedoch ein anderes Feld verwendet, funktioniert dies nicht . Der Grund dafür ist, dass die für die Bestellung erforderlichen Felder ebenfalls ausgewählt sind und jede Zeile eindeutig machen, sodass Sie nicht gruppiert werden, wie Sie es möchten. Eine schnelle Möglichkeit, dies zu beheben, besteht darin, die Bestellung zurückzusetzen:

Item.objects.values('category').annotate(Count('category')).order_by()

Dies sollte genau die gewünschten Ergebnisse liefern. So legen Sie den Namen der Anmerkung fest:

...annotate(mycount = Count('category'))...

Dann wird mycountin den Ergebnissen eine Anmerkung aufgerufen .

Alles andere an der Gruppierung war für mich sehr einfach. Weitere Informationen finden Sie in der Django-Aggregations-API .

Daniel
quelle
1
um dieselbe Aktion für das Fremdschlüsselfeld Item.objects.values ​​('category__category') auszuführen. annotate (Count ('category__category')). order_by ()
Mutant
Wie bestimmt man das Standardbestellfeld?
Bogatyr
2

Wie ist das? (Anders als langsam.)

counts= [ (c, Item.filter( category=c.id ).count()) for c in Category.objects.all() ]

Es hat den Vorteil, dass es kurz ist, auch wenn es viele Zeilen abruft.


Bearbeiten.

Die Version mit einer Abfrage. Übrigens ist dies oft schneller als SELECT COUNT (*) in der Datenbank. Probieren Sie es aus, um zu sehen.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
S.Lott
quelle
Es ist schön und kurz, aber ich möchte vermeiden, dass für jede Kategorie ein separater Datenbankaufruf erfolgt.
Sergey Golovchenko
Dies ist ein wirklich guter Ansatz für einfache Fälle. Es fällt ab, wenn Sie einen großen Datensatz haben und + limitieren (dh paginieren) möchten, ohne Tonnen nicht benötigter Daten abzurufen.
Carl Meyer
@ Carl Meyer: Stimmt - es kann für einen großen Datensatz doggy sein; Sie müssen jedoch ein Benchmarking durchführen, um sicherzugehen. Außerdem ist es nicht auf nicht unterstützte Inhalte angewiesen. Es funktioniert in der Zwischenzeit, bis die nicht unterstützten Funktionen unterstützt werden.
S.Lott