Odpowiednik Django dla liczenia i grupowania według

91

Mam model, który wygląda tak:

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

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

Chcę wybrać liczbę (tylko liczbę) elementów dla każdej kategorii, więc w SQL byłoby to tak proste:

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

Czy istnieje odpowiednik robienia tego „w sposób Django”? A może zwykły SQL jest jedyną opcją? Znam metodę count () w Django, jednak nie widzę, jak by tam pasowało group by.

Sergey Golovchenko
źródło
Możliwy duplikat Jak zapytać jako GROUP BY w django?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli 巴拿馬 文件 六四 事件 法轮功 jak to jest duplikat? to pytanie zostało zadane w 2008 roku, a to, o którym mowa, jest 2 lata później.
Sergey Golovchenko
Obecny konsensus to „jakość”: < meta.stackexchange.com/questions/147643/… > Ponieważ „jakość” nie jest mierzalna, po prostu przechodzę przez pozytywne głosy. ;-) Prawdopodobnie sprowadza się to do tego, które pytanie trafiło w najlepsze słowa kluczowe Google dla początkujących w tytule.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

132

Oto, jak właśnie odkryłem, jak to zrobić za pomocą interfejsu API agregacji Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Michał
źródło
3
jak większość rzeczy w Django, żadna z tych rzeczy nie ma sensu patrzeć, ale (w przeciwieństwie do większości rzeczy w Django) kiedy już spróbowałem, było niesamowicie: P
jsh
3
pamiętaj, że musisz użyć order_by()jeśli 'category'nie jest to domyślna kolejność. (Zobacz bardziej wyczerpującą odpowiedź Daniela.)
Rick Westera
Powodem, dla którego to działa, jest to, że .annotate()działa nieco inaczej po a.values() : „Jednak gdy klauzula values ​​() jest używana do ograniczenia kolumn zwracanych w zestawie wyników, metoda oceny adnotacji jest nieco inna. Zamiast zwracać adnotację wynik dla każdego wyniku w oryginalnym zestawie QuerySet, oryginalne wyniki są grupowane według unikalnych kombinacji pól określonych w klauzuli values ​​(). "
mgalgs
58

( Aktualizacja : Pełna obsługa agregacji ORM jest teraz zawarta w Django 1.1 . Zgodnie z poniższym ostrzeżeniem o używaniu prywatnych API, udokumentowana tutaj metoda nie działa już w wersjach Django po 1.1. jeśli używasz wersji 1.1 lub nowszej, i tak powinieneś użyć prawdziwego interfejsu API agregacji ).

Podstawowe wsparcie agregacji było już dostępne w wersji 1.0; jest po prostu nieudokumentowany, nieobsługiwany i nie ma jeszcze przyjaznego interfejsu API. Ale oto, jak możesz z niego korzystać, dopóki nie nadejdzie 1.1 (na własne ryzyko i mając pełną świadomość, że atrybut query.group_by nie jest częścią publicznego interfejsu API i może się zmienić):

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

Jeśli następnie wykonasz iterację po query_set, każda zwrócona wartość będzie słownikiem z kluczem „category” i kluczem „count”.

Nie musisz tu zamawiać według -count, jest to dołączone tylko po to, aby zademonstrować, jak to się robi (musi to być zrobione w wywołaniu .extra (), a nie gdzie indziej w łańcuchu konstrukcji queryset). Równie dobrze możesz powiedzieć count (id) zamiast count (1), ale to drugie może być bardziej wydajne.

Zauważ również, że podczas ustawiania .query.group_by, wartości muszą być rzeczywistymi nazwami kolumn DB („category_id”), a nie nazwami pól Django („kategoria”). Dzieje się tak, ponieważ dostosowujesz wewnętrzne kwerendy na poziomie, na którym wszystko jest w terminach DB, a nie w terminach Django.

Carl Meyer
źródło
+1 za starą metodę. Nawet jeśli obecnie nie jest obsługiwany, jest to co najmniej pouczające. Naprawdę niesamowite.
nalot
Zapoznaj się z interfejsem API agregacji Django pod adresem docs.djangoproject.com/en/dev/topics/db/aggregation/ ... można za jego pomocą wykonać inne złożone zadania, znajdziesz tam kilka potężnych przykładów.
serfer2
@ serfer2 tak, te dokumenty są już połączone od początku tej odpowiedzi.
Carl Meyer,
56

Ponieważ byłem trochę zdezorientowany, jak działa grupowanie w Django 1.1, pomyślałem, że opowiem tutaj o tym, jak dokładnie go używasz. Po pierwsze, powtórzyć to, co powiedział Michael:

Oto, jak właśnie odkryłem, jak to zrobić za pomocą interfejsu API agregacji Django 1.1:

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

Pamiętaj też, że musisz from django.db.models import Count!

Spowoduje to wybranie tylko kategorii, a następnie doda adnotację o nazwie category__count. W zależności od kolejności domyślnej może to być wszystko, czego potrzebujesz, ale jeśli w kolejności domyślnej używane jest inne pole category, nie zadziała . Powodem tego jest to, że pola wymagane do zamówienia są również zaznaczone i sprawiają, że każdy wiersz jest unikalny, więc nie otrzymasz pogrupowanych rzeczy tak, jak chcesz. Szybkim sposobem rozwiązania tego problemu jest zresetowanie kolejności:

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

Powinno to przynieść dokładnie takie rezultaty, jakie chcesz. Aby ustawić nazwę adnotacji, możesz użyć:

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

Następnie otrzymasz adnotację wywołaną mycountw wynikach.

Wszystko inne związane z grupowaniem było dla mnie bardzo proste. Aby uzyskać bardziej szczegółowe informacje, sprawdź interfejs API agregacji Django .

Daniel
źródło
1
wykonać ten sam zestaw akcji na polu klucza obcego Item.objects.values ​​('category__category'). annotate (Count ('category__category')). order_by ()
Mutant
Jak określić, jakie jest domyślne pole kolejności?
Bogatyr
2

Jak to jest? (Inaczej niż wolno.)

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

Ma tę zaletę, że jest krótki, nawet jeśli pobiera wiele wierszy.


Edytować.

Wersja z jednym zapytaniem. BTW, jest to często szybsze niż SELECT COUNT (*) w bazie danych. Spróbuj to zobaczyć.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
S.Lott
źródło
Jest ładny i krótki, ale chciałbym uniknąć oddzielnego wywołania bazy danych dla każdej kategorii.
Sergey Golovchenko
To naprawdę dobre podejście do prostych przypadków. Spada, gdy masz duży zestaw danych i chcesz zamówić + limit (tj. Paginację) zgodnie z liczbą, bez ściągania ton niepotrzebnych danych.
Carl Meyer,
@Carl Meyer: Prawda - może być doggy dla dużego zbioru danych; musisz jednak wykonać benchmark, aby być tego pewnym. Ponadto nie opiera się na nieobsługiwanych elementach; działa w międzyczasie, dopóki nieobsługiwane funkcje są obsługiwane.
S.Lott