148 votes

Comment faire SELECT COUNT(*) GROUP BY et ORDER BY en Django?

Je suis en train d'utiliser un modèle de transaction pour suivre tous les événements passant à travers le système

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

Comment puis-je obtenir les 5 acteurs principaux de mon système?

En sql, cela sera essentiellement

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

281voto

Alvaro Points 1441

Selon la documentation, vous devriez utiliser:

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

values() : spécifie quelles colonnes vont être utilisées pour le "group by"

Docs Django:

"Lorsqu'une clause values() est utilisée pour contraindre les colonnes qui sont retournées dans le jeu de résultats, la méthode d'évaluation des annotations est légèrement différente. Au lieu de retourner un résultat annoté pour chaque résultat dans le QuerySet original, les résultats originaux sont regroupés en fonction des combinaisons uniques des champs spécifiés dans la clause values()"

annotate() : spécifie une opération sur les valeurs groupées

Docs Django:

La deuxième façon de générer des valeurs de résumé est de générer un résumé indépendant pour chaque objet dans un QuerySet. Par exemple, si vous récupérez une liste de livres, vous voudrez peut-être savoir combien d'auteurs ont contribué à chaque livre. Chaque livre a une relation many-to-many avec l'auteur; nous voulons résumer cette relation pour chaque livre dans le QuerySet.

Les résumés par objet peuvent être générés en utilisant la clause annotate(). Lorsqu'une clause annotate() est spécifiée, chaque objet dans le QuerySet sera annoté avec les valeurs spécifiées.

La clause order by est explicite d'elle-même.

En résumé: vous faites un group by, en générant un queryset d'auteurs, ajoutez l'annotation (ceci ajoutera un champ supplémentaire aux valeurs retournées) et enfin, vous les classez par cette valeur

Référez-vous à https://docs.djangoproject.com/en/dev/topics/db/aggregation/ pour plus d'informations

Bon à noter: si vous utilisez Count, la valeur passée à Count n'affecte pas l'agrégation, juste le nom donné à la valeur finale. L'agrégateur regroupe par combinaisons uniques des valeurs (comme mentionné ci-dessus), pas par la valeur passée à Count. Les requêtes suivantes sont les mêmes:

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

58voto

Krzysiek Points 2277

Tout comme @Alvaro a répondu à l'équivalent direct de Django pour l'instruction GROUP BY:

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

est à travers l'utilisation des méthodes values() et annotate() comme suit:

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

Cependant, une autre chose doit être soulignée:

Si le modèle a un tri par défaut défini dans class Meta, la clause .order_by() est obligatoire pour obtenir des résultats corrects. Vous ne pouvez pas la sauter même si aucun tri n'est prévu.

De plus, pour un code de haute qualité, il est conseillé de toujours mettre une clause .order_by() après annotate(), même s'il n'y a pas de class Meta: ordering. Cette approche rendra la déclaration pérenne : elle fonctionnera comme prévu, quelles que soient les modifications futures apportées à class Meta: ordering.


Laissez-moi vous fournir un exemple. Si le modèle avait:

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

Alors une telle approche NE FONCTIONNERAIT PAS:

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

C'est parce que Django effectue un GROUP BY supplémentaire sur chaque champ dans class Meta: ordering

Si vous affichez la requête:

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

Il sera clair que l'agrégation ne fonctionnerait PAS comme prévu et donc la clause .order_by() doit être utilisée pour clarifier ce comportement et obtenir des résultats d'agrégation appropriés.

Voir: Interaction with default ordering or order_by() dans la documentation officielle de Django.

4voto

user5510975 Points 307

Si vous voulez inverser l'ordre (passer d'une plus grande valeur à une plus petite valeur), il suffit d'utiliser le signe - moins.

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

0voto

En fait, je n'arrive pas à utiliser les fonctions values() et annotations combinées.

Le code ci-dessous n'a pas fonctionné lorsque j'ai essayé :

from django.db.models import Count

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

donc, je préfère le type de code ci-dessous pour regrouper :

get_posts(user_id):
user = Post.objects.all().\
    values('posted_by').\
    annotate(count=Count("post_id"))
return user

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X