¿Cómo hacer SELECT COUNT (*) GROUP BY y ORDER BY en Django?

99

Estoy usando un modelo de transacción para realizar un seguimiento de todos los eventos que pasan por el 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() 
    ......

¿Cómo obtengo los 5 mejores actores en mi sistema?

En sql básicamente será

SELECT actor, COUNT(*) as total 
FROM Transaction 
GROUP BY actor 
ORDER BY total DESC
totoromeow
fuente

Respuestas:

181

Según la documentación, debe utilizar:

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

valores (): especifica qué columnas se utilizarán para "agrupar por"

Documentos de Django:

"Cuando se utiliza una cláusula de valores () para restringir las columnas que se devuelven en el conjunto de resultados, el método para evaluar las anotaciones es ligeramente diferente. En lugar de devolver un resultado anotado para cada resultado en el QuerySet original, los resultados originales se agrupan según a las combinaciones únicas de los campos especificados en la cláusula values ​​() "

annotate (): especifica una operación sobre los valores agrupados

Documentos de Django:

La segunda forma de generar valores de resumen es generar un resumen independiente para cada objeto en un QuerySet. Por ejemplo, si está recuperando una lista de libros, es posible que desee saber cuántos autores contribuyeron a cada libro. Cada libro tiene una relación de varios a varios con el autor; queremos resumir esta relación para cada libro en el QuerySet.

Los resúmenes por objeto se pueden generar usando la cláusula annotate (). Cuando se especifica una cláusula annotate (), cada objeto en el QuerySet se anotará con los valores especificados.

El orden por cláusula se explica por sí mismo.

Para resumir: agrupa por, generando un conjunto de consultas de autores, agrega la anotación (esto agregará un campo adicional a los valores devueltos) y finalmente, los ordena por este valor

Consulte https://docs.djangoproject.com/en/dev/topics/db/aggregation/ para obtener más información.

Es bueno tener en cuenta: si usa Count, el valor pasado a Count no afecta la agregación, solo el nombre dado al valor final. El agregador agrupa por combinaciones únicas de los valores (como se mencionó anteriormente), no por el valor pasado a Count. Las siguientes consultas son las mismas:

Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total')
Transaction.objects.all().values('actor').annotate(total=Count('id')).order_by('total')
Álvaro
fuente
Para mí funcionó como Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total'), no olvides importar Count desde django.db.models. Gracias
Ivancho
3
Es bueno tener en cuenta: si usa Count(y tal vez otros agregadores), el valor pasado a Countno afecta la agregación, solo el nombre dado al valor final. El agregador agrupa por combinaciones únicas de values(como se mencionó anteriormente), no por el valor pasado a Count.
kronosapiens
¡Incluso puede usar esto para conjuntos de consultas de resultados de búsqueda de Postgres para obtener facetas!
yekta
2
@kronosapiens Sí lo afecta, al menos hoy en día (estoy usando Django 2.1.4). En el ejemplo, ¿ totalse da el nombre y el recuento utilizado en sql es lo COUNT('actor')que en este caso no importa, pero si values('x', 'y').annotate(count=Count('x')), por ejemplo , obtendrá COUNT(x), no COUNT(*)o COUNT(x, y), simplemente lo probó?./manage.py shell
timdiels
35

Al igual que @Alvaro ha respondido el equivalente directo de Django para la GROUP BYdeclaración:

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

es mediante el uso de values()y annotate()métodos de la siguiente manera:

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

Sin embargo, hay que señalar una cosa más:

Si el modelo tiene un orden predeterminado definido en class Meta, la .order_by()cláusula es obligatoria para obtener resultados adecuados. Simplemente no puede omitirlo incluso cuando no se pretende realizar un pedido.

Además, para un código de alta calidad, se recomienda colocar siempre una .order_by()cláusula después annotate(), incluso cuando no hay class Meta: ordering. Dicho enfoque hará que la declaración esté preparada para el futuro: funcionará tal como se pretendía, independientemente de cualquier cambio futuro en class Meta: ordering.


Déjame darte un ejemplo. Si el modelo tuviera:

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

Entonces tal enfoque NO funcionaría:

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

Eso es porque Django realiza más GROUP BYen cada campo enclass Meta: ordering

Si imprime la 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"

Quedará claro que la agregación NO funcionaría según lo previsto y, por lo tanto, la .order_by()cláusula debe usarse para borrar este comportamiento y obtener los resultados de agregación adecuados.

Ver: Interacción con el orden predeterminado o order_by () en la documentación oficial de Django.

Krzysiek
fuente
3
.order_by()me salvó orderingen Meta.
Babken Vardanyan