Django équivalent pour count et group by

91

J'ai un modèle qui ressemble à ceci:

class Category(models.Model):
    name = models.CharField(max_length=60)

class Item(models.Model):
    name = models.CharField(max_length=60)
    category = models.ForeignKey(Category)

Je veux sélectionner le nombre (juste le nombre) d'éléments pour chaque catégorie, donc en SQL, ce serait aussi simple que ceci:

select category_id, count(id) from item group by category_id

Y a-t-il un équivalent de faire cela "à la manière Django"? Ou le SQL brut est-il la seule option? Je connais la méthode count () de Django, mais je ne vois pas comment group by s'y adapterait.

Sergey Golovchenko
la source
Double
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli 巴拿馬 文件 六四 事件 法轮功 comment est-ce un doublon? cette question a été posée en 2008, et celle à laquelle vous faites référence est de 2 ans plus tard.
Sergey Golovchenko
Le consensus actuel est de clôturer par «qualité»: < meta.stackexchange.com/questions/147643/… > Puisque la «qualité» n'est pas mesurable, je me contente de voter pour. ;-) Cela dépend probablement de la question qui a frappé les meilleurs mots-clés Google débutants sur le titre.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

131

Voici, comme je viens de le découvrir, comment faire cela avec l'API d'agrégation Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Michael
la source
3
comme la plupart des choses dans Django, rien de tout cela n'a vraiment de sens à regarder, mais (contrairement à la plupart des choses dans Django) une fois que je l'ai essayé, c'était génial: P
jsh
3
notez que vous devez utiliser order_by()if 'category'n'est pas la commande par défaut. (Voir la réponse plus complète de Daniel.)
Rick Westera
La raison pour laquelle cela fonctionne est que cela .annotate()fonctionne légèrement différemment après un.values() : "Cependant, lorsqu'une clause values ​​() est utilisée pour contraindre les colonnes renvoyées dans le jeu de résultats, la méthode d'évaluation des annotations est légèrement différente. Au lieu de renvoyer un annoté résultat pour chaque résultat du QuerySet d'origine, les résultats d'origine sont regroupés en fonction des combinaisons uniques des champs spécifiés dans la clause values ​​(). "
mgalgs
58

( Mise à jour : la prise en charge complète de l'agrégation ORM est désormais incluse dans Django 1.1 . Fidèle à l'avertissement ci-dessous concernant l'utilisation des API privées, la méthode documentée ici ne fonctionne plus dans les versions post-1.1 de Django. Je n'ai pas cherché à comprendre pourquoi; si vous êtes sur 1.1 ou version ultérieure, vous devez quand même utiliser la véritable API d'agrégation .)

Le support d'agrégation de base était déjà présent dans la version 1.0; il est simplement non documenté, non pris en charge et n'a pas encore d'API conviviale en plus. Mais voici comment vous pouvez quand même l'utiliser jusqu'à l'arrivée de la version 1.1 (à vos risques et périls, et en sachant parfaitement que l'attribut query.group_by ne fait pas partie d'une API publique et pourrait changer):

query_set = Item.objects.extra(select={'count': 'count(1)'}, 
                               order_by=['-count']).values('count', 'category')
query_set.query.group_by = ['category_id']

Si vous parcourez ensuite query_set, chaque valeur renvoyée sera un dictionnaire avec une clé "category" et une clé "count".

Vous n'êtes pas obligé de classer par -count ici, c'est juste inclus pour montrer comment cela se fait (cela doit être fait dans l'appel .extra (), pas ailleurs dans la chaîne de construction du jeu de requêtes). De plus, vous pouvez tout aussi bien dire count (id) au lieu de count (1), mais ce dernier peut être plus efficace.

Notez également que lors de la définition de .query.group_by, les valeurs doivent être des noms de colonne DB réels ('category_id') et non des noms de champs Django ('category'). C'est parce que vous peaufinez les internes de la requête à un niveau où tout est en termes de base de données, pas en termes de Django.

Carl Meyer
la source
+1 pour l'ancienne méthode. Même s'il n'est actuellement pas pris en charge, c'est pour le moins instructif. Incroyable, vraiment.
frappe aérienne du
Jetez un œil à l'API d'agrégation Django sur docs.djangoproject.com/en/dev/topics/db/aggregation/ ... d' autres tâches complexes peuvent être effectuées avec, vous y trouverez des exemples puissants.
serfer2
@ serfer2 oui, ces documents sont déjà liés à partir du haut de cette réponse.
Carl Meyer
56

Étant donné que j'étais un peu confus sur le fonctionnement du regroupement dans Django 1.1, j'ai pensé expliquer ici comment vous l'utilisez exactement. Tout d'abord, pour répéter ce que Michael a dit:

Voici, comme je viens de le découvrir, comment faire cela avec l'API d'agrégation Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))

Notez également que vous devez from django.db.models import Count!

Cela sélectionnera uniquement les catégories, puis ajoutera une annotation appelée category__count. Selon la commande par défaut, cela peut être tout ce dont vous avez besoin, mais si la commande par défaut utilise un champ autre que categorycelui-ci ne fonctionnera pas . La raison en est que les champs requis pour la commande sont également sélectionnés et rendent chaque ligne unique, de sorte que vous ne serez pas regroupé comme vous le souhaitez. Un moyen rapide de résoudre ce problème consiste à réinitialiser la commande:

Item.objects.values('category').annotate(Count('category')).order_by()

Cela devrait produire exactement les résultats que vous souhaitez. Pour définir le nom de l'annotation, vous pouvez utiliser:

...annotate(mycount = Count('category'))...

Ensuite, vous aurez une annotation appelée mycountdans les résultats.

Tout le reste concernant le regroupement était très simple pour moi. N'oubliez pas de consulter l' API d'agrégation Django pour des informations plus détaillées.

Daniel
la source
1
pour effectuer le même ensemble d'actions sur le champ de clé étrangère Item.objects.values ​​('category__category'). annotate (Count ('category__category')). order_by ()
Mutant
Comment déterminer quel est le champ de classement par défaut?
Bogatyr
2

Comment c'est? (Autre que lent.)

counts= [ (c, Item.filter( category=c.id ).count()) for c in Category.objects.all() ]

Il a l'avantage d'être court, même s'il récupère beaucoup de lignes.


Éditer.

La version à une requête. BTW, c'est souvent plus rapide que SELECT COUNT (*) dans la base de données. Essayez-le pour voir.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
S.Lott
la source
C'est joli et court, mais j'aimerais éviter d'avoir un appel de base de données distinct pour chaque catégorie.
Sergey Golovchenko
C'est une très bonne approche pour les cas simples. Il tombe lorsque vous avez un grand ensemble de données, et que vous voulez commander + limite (c'est-à-dire paginer) en fonction d'un décompte, sans extraire des tonnes de données inutiles.
Carl Meyer
@Carl Meyer: Vrai - cela peut être doggy pour un grand ensemble de données; vous devez cependant effectuer une analyse comparative pour en être sûr. En outre, il ne repose pas non plus sur des éléments non pris en charge; il fonctionne dans l'intervalle jusqu'à ce que les fonctionnalités non prises en charge soient prises en charge.
S.Lott