Jak zrobić SELECT COUNT (*) GROUP BY i ORDER BY w Django?

99

Używam modelu transakcyjnego, aby śledzić wszystkie zdarzenia przechodzące przez system

class Transaction(models.Model):
    actor = models.ForeignKey(User, related_name="actor")
    acted = models.ForeignKey(User, related_name="acted", null=True, blank=True)
    action_id = models.IntegerField() 
    ......

jak zdobyć 5 najlepszych aktorów w moim systemie?

W sql w zasadzie będzie

SELECT actor, COUNT(*) as total 
FROM Transaction 
GROUP BY actor 
ORDER BY total DESC
totoromeow
źródło

Odpowiedzi:

181

Zgodnie z dokumentacją należy użyć:

from django.db.models import Count
Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total')

wartości (): określa, które kolumny będą używane do „grupowania według”

Dokumentacja Django:

„Gdy klauzula values ​​() jest używana do ograniczenia kolumn zwracanych w zestawie wyników, metoda oceny adnotacji jest nieco inna. Zamiast zwracać wynik z adnotacją dla każdego wyniku w oryginalnym zestawie QuerySet, oryginalne wyniki są grupowane według do unikalnych kombinacji pól określonych w klauzuli values ​​() "

annotate (): określa operację na zgrupowanych wartościach

Dokumentacja Django:

Drugim sposobem generowania wartości podsumowań jest generowanie niezależnego podsumowania dla każdego obiektu w QuerySet. Na przykład, jeśli pobierasz listę książek, możesz chcieć wiedzieć, ilu autorów przyczyniło się do powstania każdej książki. Każda książka ma z Autorem relację „wiele do wielu”; chcemy podsumować tę relację dla każdej książki w zestawie QuerySet.

Podsumowania dla poszczególnych obiektów można generować za pomocą klauzuli annotate (). Po określeniu klauzuli annotate () każdy obiekt w QuerySet zostanie opatrzony adnotacją z określonymi wartościami.

Kolejność według klauzuli nie wymaga wyjaśnień.

Podsumowując: grupujesz według, generujesz zestaw zapytań autorów, dodajesz adnotację (doda to dodatkowe pole do zwracanych wartości) i na koniec porządkujesz ich według tej wartości

Patrz https://docs.djangoproject.com/en/dev/topics/db/aggregation/ więcej wglądu

Warto zauważyć: jeśli używasz funkcji Count, wartość przekazana do Count nie wpływa na agregację, a jedynie nazwę nadaną wartości końcowej. Agregator grupuje według unikalnych kombinacji wartości (jak wspomniano powyżej), a nie według wartości przekazanej do Count. Następujące zapytania są takie same:

Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total')
Transaction.objects.all().values('actor').annotate(total=Count('id')).order_by('total')
Alvaro
źródło
U mnie zadziałało Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total'), nie zapomnij zaimportować Count z django.db.models. Dziękuję
Ivancho
3
Warto zauważyć: jeśli używasz Count(i być może innych agregatorów), wartość przekazana do Countnie wpływa na agregację, a jedynie nazwę nadaną wartości końcowej. Agregator grupuje według unikalnych kombinacji values(jak wspomniano powyżej), a nie według wartości przekazanej do Count.
kronosapiens
Możesz nawet użyć tego do zestawów zapytań wyników wyszukiwania postgres, aby uzyskać faceting!
yekta
2
@kronosapiens To ma na to wpływ, przynajmniej obecnie (używam Django 2.1.4). W przykładzie totalpodano nazwę i liczbę użytą w sql, COUNT('actor')która w tym przypadku nie ma znaczenia, ale jeśli np. values('x', 'y').annotate(count=Count('x'))Dostaniesz COUNT(x), nie COUNT(*)lub COUNT(x, y)właśnie wypróbowałeś./manage.py shell
timdiels
35

Tak jak @Alvaro odpowiedział na bezpośredni odpowiednik GROUP BYstwierdzenia Django :

SELECT actor, COUNT(*) AS total 
FROM Transaction 
GROUP BY actor

jest dzięki wykorzystaniu values()i annotate()metody w następujący sposób:

Transaction.objects.values('actor').annotate(total=Count('actor')).order_by()

Należy jednak zwrócić uwagę na jeszcze jedną rzecz:

Jeżeli model ma zdefiniowaną domyślną kolejność class Meta, .order_by()klauzula jest obligatoryjna dla poprawnych wyników. Po prostu nie można go pominąć, nawet jeśli nie jest planowane zamówienie.

Ponadto w przypadku kodu wysokiej jakości zaleca się zawsze umieszczać .order_by()klauzulę po annotate(), nawet jeśli nie ma class Meta: ordering. Takie podejście sprawi, że oświadczenie będzie przyszłościowe: będzie działać zgodnie z zamierzeniami, niezależnie od przyszłych zmian class Meta: ordering.


Podam przykład. Gdyby model miał:

class Transaction(models.Model):
    actor = models.ForeignKey(User, related_name="actor")
    acted = models.ForeignKey(User, related_name="acted", null=True, blank=True)
    action_id = models.IntegerField()

    class Meta:
        ordering = ['id']

Wtedy takie podejście NIE BĘDZIE działało:

Transaction.objects.values('actor').annotate(total=Count('actor'))

To dlatego, że Django wykonuje dodatkowe operacje GROUP BYna każdym polu wclass Meta: ordering

Jeśli chcesz wydrukować zapytanie:

>>> print Transaction.objects.values('actor').annotate(total=Count('actor')).query
  SELECT "Transaction"."actor_id", COUNT("Transaction"."actor_id") AS "total"
  FROM "Transaction"
  GROUP BY "Transaction"."actor_id", "Transaction"."id"

Będzie jasne, że agregacja NIE zadziała zgodnie z przeznaczeniem i dlatego .order_by()należy użyć klauzuli, aby usunąć to zachowanie i uzyskać prawidłowe wyniki agregacji.

Zobacz: Interakcja z domyślnym zamówieniem lub order_by () w oficjalnej dokumentacji Django.

Krzysiek
źródło
3
.order_by()uratował mnie przed orderingMeta.
Babken Vardanyan