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

95

J'utilise un modèle de transaction pour suivre tous les événements traversant 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 obtenir les 5 meilleurs acteurs de mon système?

Dans SQL, ce sera essentiellement

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

Réponses:

176

Selon la documentation, vous devez 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 "grouper par"

Documents Django:

"Lorsqu'une clause values ​​() est utilisée pour contraindre les colonnes renvoyées dans l'ensemble de résultats, la méthode d'évaluation des annotations est légèrement différente. Au lieu de renvoyer un résultat annoté pour chaque résultat dans le QuerySet d'origine, les résultats d'origine sont groupés selon aux combinaisons uniques des champs spécifiés dans la clause values ​​() "

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

Documents Django:

La deuxième façon de générer des valeurs récapitulatives consiste à générer un récapitulatif indépendant pour chaque objet d'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 plusieurs-à-plusieurs avec l'auteur; nous voulons résumer cette relation pour chaque livre de QuerySet.

Les résumés par objet peuvent être générés à l'aide de la clause annotate (). Lorsqu'une clause annotate () est spécifiée, chaque objet du QuerySet sera annoté avec les valeurs spécifiées.

L'ordre par clause est explicite.

Pour résumer: vous groupez par, générant un jeu de requêtes d'auteurs, ajoutez l'annotation (cela ajoutera un champ supplémentaire aux valeurs renvoyées) et enfin, vous les classez par cette valeur

Reportez-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) et non par la valeur transmise à 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')
Alvaro
la source
Pour moi, cela a fonctionné comme Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total'), n'oubliez pas d'importer Count depuis django.db.models. Merci
Ivancho
3
Bon à noter: si vous utilisez Count(et peut-être d'autres agrégateurs), la valeur transmise à Countn'affecte pas l'agrégation, juste le nom donné à la valeur finale. L'agrégateur regroupe par combinaisons uniques de values(comme mentionné ci-dessus), et non par valeur transmise à Count.
kronosapiens
Vous pouvez même l'utiliser pour les ensembles de requêtes de résultats de recherche postgres pour obtenir des facettes!
yekta
2
@kronosapiens Cela l'affecte, du moins de nos jours (j'utilise Django 2.1.4). Dans l'exemple, totalle nom est-il donné et le nombre utilisé dans sql est COUNT('actor')ce qui dans ce cas n'a pas d'importance, mais si par exemple values('x', 'y').annotate(count=Count('x')), vous obtiendrez COUNT(x), pas COUNT(*)ou COUNT(x, y), simplement essayé./manage.py shell
timdiels
33

Tout comme @Alvaro a répondu à l'équivalent direct de Django pour la GROUP BYdéclaration:

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

se fait par l'utilisation de values()et des annotate()méthodes suivantes:

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

Cependant, une dernière chose doit être soulignée:

Si le modèle a un ordre par défaut défini dans class Meta, la .order_by()clause est obligatoire pour des résultats corrects. Vous ne pouvez tout simplement pas l'ignorer même si aucune commande n'est prévue.

De plus, pour un code de haute qualité, il est conseillé de toujours mettre une .order_by()clause après annotate(), même s'il n'y en a pas class Meta: ordering. Une telle approche rendra la déclaration à l'épreuve du temps: elle fonctionnera comme prévu, indépendamment des modifications futures apportées à class Meta: ordering.


Laissez-moi vous donner 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 fonctionnera PAS:

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

C'est parce que Django effectue des performances supplémentaires GROUP BYsur chaque champ declass Meta: ordering

Si vous imprimez 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 fonctionnera PAS comme prévu et par conséquent, la .order_by()clause doit être utilisée pour effacer ce comportement et obtenir des résultats d'agrégation appropriés.

Voir: Interaction avec la commande par défaut ou order_by () dans la documentation officielle de Django.

Krzysiek
la source
3
.order_by()m'a sauvé de orderingMeta.
Babken Vardanyan