Como fazer SELECT COUNT (*) GROUP BY e ORDER BY no Django?

95

Estou usando um modelo de transação para rastrear todos os eventos que ocorrem no sistema

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() 
    ......

como faço para obter os 5 principais atores em meu sistema?

Em sql será basicamente

SELECT actor, COUNT(*) as total 
FROM Transaction 
GROUP BY actor 
ORDER BY total DESC
Totoromeow
fonte

Respostas:

176

De acordo com a documentação, você deve usar:

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

valores (): especifica quais colunas serão usadas para "agrupar por"

Django docs:

"Quando uma cláusula values ​​() é usada para restringir as colunas que são retornadas no conjunto de resultados, o método de avaliação das anotações é ligeiramente diferente. Em vez de retornar um resultado anotado para cada resultado no QuerySet original, os resultados originais são agrupados de acordo com às combinações exclusivas dos campos especificados na cláusula values ​​() "

annotate (): especifica uma operação sobre os valores agrupados

Django docs:

A segunda maneira de gerar valores de resumo é gerar um resumo independente para cada objeto em um QuerySet. Por exemplo, se você está recuperando uma lista de livros, pode querer saber quantos autores contribuíram para cada livro. Cada livro tem uma relação de muitos para muitos com o autor; queremos resumir essa relação para cada livro no QuerySet.

Os resumos por objeto podem ser gerados usando a cláusula annotate (). Quando uma cláusula annotate () é especificada, cada objeto no QuerySet será anotado com os valores especificados.

A cláusula order by é autoexplicativa.

Para resumir: você agrupa, gerando um conjunto de consultas de autores, adiciona a anotação (isso adicionará um campo extra aos valores retornados) e finalmente, você os ordena por este valor

Consulte https://docs.djangoproject.com/en/dev/topics/db/aggregation/ para obter mais informações

É bom observar: se estiver usando Count, o valor passado para Count não afeta a agregação, apenas o nome dado ao valor final. O agregador agrupa por combinações exclusivas dos valores (conforme mencionado acima), não pelo valor passado para Count. As seguintes consultas são iguais:

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
fonte
Para mim funcionou como Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total'), não se esqueça de importar Count de django.db.models. Obrigado
Ivancho
3
É bom observar: se estiver usando Count(e talvez outros agregadores), o valor passado para Countnão afeta a agregação, apenas o nome dado ao valor final. O agregador agrupa por combinações exclusivas de values(como mencionado acima), não pelo valor passado para Count.
kronosapiens
Você pode até usar isso para conjuntos de consulta de resultados de pesquisa do postgres para obter facetação!
yekta
2
@kronosapiens Afeta, pelo menos hoje em dia (estou usando Django 2.1.4). No exemplo, totalé o nome fornecido e a contagem usada em sql é COUNT('actor')que, neste caso, não importa, mas se values('x', 'y').annotate(count=Count('x')), por exemplo , você obterá COUNT(x), não COUNT(*)ou COUNT(x, y), apenas tentei em./manage.py shell
timdiels
33

Assim como @Alvaro respondeu ao equivalente direto do Django para a GROUP BYdeclaração:

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

é por meio do uso dos métodos values()e da annotate()seguinte maneira:

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

No entanto, mais uma coisa deve ser apontada:

Se o modelo tiver uma ordenação padrão definida em class Meta, a .order_by()cláusula é obrigatória para resultados adequados. Você simplesmente não pode ignorá-lo, mesmo quando nenhum pedido é pretendido.

Além disso, para um código de alta qualidade, é aconselhável sempre colocar uma .order_by()cláusula após annotate(), mesmo quando não houver class Meta: ordering. Essa abordagem tornará a declaração à prova de futuro: ela funcionará exatamente como pretendido, independentemente de quaisquer alterações futuras em class Meta: ordering.


Deixe-me dar um exemplo. Se o modelo tivesse:

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']

Então, essa abordagem NÃO funcionaria:

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

Isso porque o Django executa mais GROUP BYem cada campo doclass Meta: ordering

Se você imprimir a consulta:

>>> 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"

Ficará claro que a agregação NÃO funcionaria conforme o esperado e, portanto, a .order_by()cláusula deve ser usada para limpar esse comportamento e obter resultados de agregação adequados.

Veja: Interação com ordenação padrão ou order_by () na documentação oficial do Django.

Krzysiek
fonte
3
.order_by()me salvou de orderingMeta.
Babken Vardanyan