Comment interroger en tant que GROUP BY dans Django?

333

Je recherche un modèle:

Members.objects.all()

Et ça revient:

Eric, Salesman, X-Shop
Freddie, Manager, X2-Shop
Teddy, Salesman, X2-Shop
Sean, Manager, X2-Shop

Ce que je veux, c'est connaître la meilleure façon Django de lancer une group_byrequête dans ma base de données, comme:

Members.objects.all().group_by('designation')

Ce qui ne fonctionne pas, bien sûr. Je sais que nous pouvons faire quelques astuces django/db/models/query.py, mais je suis simplement curieux de savoir comment le faire sans patcher.

Simplyharsh
la source

Réponses:

484

Si vous souhaitez effectuer une agrégation, vous pouvez utiliser les fonctionnalités d'agrégation de l'ORM :

from django.db.models import Count
Members.objects.values('designation').annotate(dcount=Count('designation'))

Il en résulte une requête similaire à

SELECT designation, COUNT(designation) AS dcount
FROM members GROUP BY designation

et la sortie serait de la forme

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
Guðmundur H
la source
6
@ Harry: vous pouvez l'enchaîner. Quelque chose comme:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli
57
J'ai une question, cette requête ne renvoie que la désignation et le décompte, que se passe-t-il si je veux également obtenir d'autres valeurs de la table?
AJ
19
Notez que si votre tri est un champ autre que la désignation, il ne fonctionnera pas sans réinitialiser le tri. Voir stackoverflow.com/a/1341667/202137
Gidgidonihah
12
@Gidgidonihah True, l'exemple devrait lireMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix
7
J'ai une question, cette requête ne renvoie que la désignation et le décompte, que se passe-t-il si je veux également obtenir d'autres valeurs de la table?
Yann
55

Une solution simple, mais pas la bonne, consiste à utiliser du SQL brut :

results = Members.objects.raw('SELECT * FROM myapp_members GROUP BY designation')

Une autre solution consiste à utiliser la group_bypropriété:

query = Members.objects.all().query
query.group_by = ['designation']
results = QuerySet(query=query, model=Members)

Vous pouvez maintenant parcourir la variable des résultats pour récupérer vos résultats. Notez que cela group_byn'est pas documenté et pourrait être modifié dans la future version de Django.

Et ... pourquoi voulez-vous l'utiliser group_by? Si vous n'utilisez pas d'agrégation, vous pouvez utiliser order_bypour obtenir un résultat similaire.

Michael
la source
Pouvez-vous me dire comment le faire en utilisant order_by ??
Simplyharsh
2
Bonjour, si vous n'utilisez pas l'agrégation, vous pouvez émuler group_by en utilisant un order_by et éliminer les entrées dont vous n'avez pas besoin. Bien sûr, il s'agit d'une émulation et n'est utilisable que lorsque vous n'utilisez pas beaucoup de données. Puisqu'il n'a pas parlé d'agrégation, j'ai pensé que cela pourrait être une solution.
Michael
Hé c'est génial - pouvez-vous s'il vous plaît expliquer comment utiliser execute_sql cela ne semble pas fonctionner ..
rh0dium
8
Notez que cela ne fonctionne plus sur Django 1.9. stackoverflow.com/questions/35558120/…
grokpot
1
C'est une sorte de méthode hack-ish pour utiliser l'ORM. Vous ne devriez pas avoir à instancier de nouveaux ensembles de requêtes en passant les anciens manuellement.
Ian Kirkpatrick
33

Vous pouvez également utiliser la regroupbalise de modèle pour regrouper par attributs. De la documentation:

cities = [
    {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
    {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
    {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
    {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
    {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

...

{% regroup cities by country as country_list %}

<ul>
    {% for country in country_list %}
        <li>{{ country.grouper }}
            <ul>
            {% for city in country.list %}
                <li>{{ city.name }}: {{ city.population }}</li>
            {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

Ressemble à ça:

  • Inde
    • Mumbai: 19 000 000
    • Calcutta: 15 000 000
  • Etats-Unis
    • New York: 20 000 000
    • Chicago: 7 000 000
  • Japon
    • Tokyo: 33 000 000

Il fonctionne également sur QuerySets je crois.

source: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#regroup

modifier: notez que la regroupbalise ne fonctionne pas comme vous vous y attendez si votre liste de dictionnaires n'est pas triée par clé. Cela fonctionne de manière itérative. Triez donc votre liste (ou ensemble de requêtes) par la clé du mérou avant de la passer à la regroupbalise.

inostia
la source
1
C'est parfait! J'ai beaucoup cherché un moyen simple de le faire. Et cela fonctionne également sur les ensembles de requêtes, c'est comme ça que je l'ai utilisé.
CarmenA
1
c'est totalement faux si vous lisez à partir de la base de données un grand ensemble de données, puis utilisez simplement des valeurs agrégées.
Sławomir Lenart
@ SławomirLenart bien sûr, cela pourrait ne pas être aussi efficace qu'une simple requête DB. Mais pour les cas d'utilisation simples, cela peut être une bonne solution
inostia
Cela fonctionnera si le résultat affiché dans le modèle. Mais, pour JsonResponse ou autre réponse indirecte. cette solution ne fonctionnera pas.
Willy satrio nugroho
1
@Willysatrionugroho si vous vouliez le faire dans une vue, par exemple, stackoverflow.com/questions/477820/… pourrait fonctionner pour vous
inostia
7

Vous devez faire du SQL personnalisé comme illustré dans cet extrait:

SQL personnalisé via une sous-requête

Ou dans un gestionnaire personnalisé comme indiqué dans les documents Django en ligne:

Ajout de méthodes Manager supplémentaires

Van Gale
la source
1
Type de solution aller-retour. Je l'aurais utilisé si j'avais eu une utilisation prolongée de cela. Mais ici, j'ai juste besoin du nombre de membres par désignation, c'est tout.
Simplyharsh
Aucun problème. J'ai pensé à mentionner les fonctionnalités d'agrégation 1.1, mais j'ai supposé que vous utilisiez la version finale :)
Van Gale
Il s'agit d'utiliser des requêtes brutes, qui montrent la faiblesse de l'ORM de Django.
Sławomir Lenart
5

Django ne prend pas en charge les groupes libres par requêtes . Je l'ai appris de très mauvaise façon. ORM n'est pas conçu pour prendre en charge des choses comme ce que vous voulez faire, sans utiliser de SQL personnalisé. Vous êtes limité à:

  • SQL RAW (c'est-à-dire MyModel.objects.raw ())
  • cr.execute phrases (et une analyse à la main du résultat).
  • .annotate() (le regroupement par phrases est effectué dans le modèle enfant pour .annotate (), dans des exemples comme l'agrégation lines_count = Count ('lines'))).

Sur un ensemble de requêtes, qsvous pouvez appeler qs.query.group_by = ['field1', 'field2', ...]mais il est risqué si vous ne savez pas quelle requête éditez-vous et n'avez aucune garantie que cela fonctionnera et ne cassera pas les internes de l'objet QuerySet. De plus, c'est une API interne (non documentée) à laquelle vous ne devez pas accéder directement sans risquer que le code ne soit plus compatible avec les futures versions de Django.

Luis Masuelli
la source
en effet, vous êtes limité non seulement en group-by gratuit, alors essayez SQLAlchemy au lieu de Django ORM.
Sławomir Lenart
5

Il existe un module qui vous permet de regrouper les modèles Django et de continuer à travailler avec un QuerySet dans le résultat: https://github.com/kako-nawao/django-group-by

Par exemple:

from django_group_by import GroupByMixin

class BookQuerySet(QuerySet, GroupByMixin):
    pass

class Book(Model):
    title = TextField(...)
    author = ForeignKey(User, ...)
    shop = ForeignKey(Shop, ...)
    price = DecimalField(...)

class GroupedBookListView(PaginationMixin, ListView):
    template_name = 'book/books.html'
    model = Book
    paginate_by = 100

    def get_queryset(self):
        return Book.objects.group_by('title', 'author').annotate(
            shop_count=Count('shop'), price_avg=Avg('price')).order_by(
            'name', 'author').distinct()

    def get_context_data(self, **kwargs):
        return super().get_context_data(total_count=self.get_queryset().count(), **kwargs)

«book / books.html»

<ul>
{% for book in object_list %}
    <li>
        <h2>{{ book.title }}</td>
        <p>{{ book.author.last_name }}, {{ book.author.first_name }}</p>
        <p>{{ book.shop_count }}</p>
        <p>{{ book.price_avg }}</p>
    </li>
{% endfor %}
</ul>

La différence des annotate/ de aggregatebase des requêtes Django est l'utilisation des attributs d'un domaine connexe, par exemple book.author.last_name.

Si vous avez besoin des PK des instances qui ont été regroupées, ajoutez l'annotation suivante:

.annotate(pks=ArrayAgg('id'))

REMARQUE: ArrayAggest une fonction spécifique à Postgres, disponible à partir de Django 1.9: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg

Risadinha
la source
Ce django-group-by est une alternative à la valuesméthode. C'est à des fins différentes, je pense.
LShi
1
@LShi Ce n'est pas une alternative aux valeurs, bien sûr que non. valuesest un SQL selecttandis que group_byest un SQL group by(comme son nom l'indique ...). Pourquoi le downvote? Nous utilisons ce code en production pour implémenter des group_byinstructions complexes .
Risadinha
Son doc dit group_by"se comporte principalement comme la méthode des valeurs, mais avec une différence ..." Le doc ne mentionne pas SQL GROUP BYet le cas d'utilisation qu'il fournit ne suggère pas qu'il ait quoi que ce soit à voir avec SQL GROUP BY. Je retirerai le vote négatif lorsque quelqu'un aura clarifié cela, mais ce document est vraiment trompeur.
LShi
Après avoir lu le document pourvalues , j'ai trouvé que je manquais que cela valuesfonctionne comme un GROUP BY. C'est de ma faute. Je pense qu'il est plus simple à utiliser itertools.groupbyque ce groupe de django quand il valuesest insuffisant.
LShi
1
Il est impossible de faire ce qui group byprécède avec un simple valuesappel - avec ou sans annotateet sans tout récupérer de la base de données. Votre suggestion de itertools.groupbytravaux pour les petits ensembles de données mais pas pour plusieurs milliers d'ensembles de données que vous souhaitez probablement mettre en page. Bien sûr, à ce stade, vous devrez de toute façon penser à un index de recherche spécial qui contient des données préparées (déjà groupées).
Risadinha
0

Le document indique que vous pouvez utiliser des valeurs pour regrouper l'ensemble de requêtes.

class Travel(models.Model):
    interest = models.ForeignKey(Interest)
    user = models.ForeignKey(User)
    time = models.DateTimeField(auto_now_add=True)

# Find the travel and group by the interest:

>>> Travel.objects.values('interest').annotate(Count('user'))
<QuerySet [{'interest': 5, 'user__count': 2}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited for 2 times, 
# and the interest(id=6) had only been visited for 1 time.

>>> Travel.objects.values('interest').annotate(Count('user', distinct=True)) 
<QuerySet [{'interest': 5, 'user__count': 1}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited by only one person (but this person had 
#  visited the interest for 2 times

Vous pouvez trouver tous les livres et les regrouper par nom en utilisant ce code:

Book.objects.values('name').annotate(Count('id')).order_by() # ensure you add the order_by()

Vous pouvez regarder une feuille de cheet ici .

ramwin
la source
-1

Si je ne me trompe pas, vous pouvez utiliser, quel que soit l'ensemble de requêtes .group_by = [' field ']

Reed Jones
la source
8
Ce n'est pas le cas, du moins dans Django 1.6: l'objet 'QuerySet' n'a pas d'attribut 'group_by'
Facundo Olano
1
Une utilisation appropriée pourrait être queryset.query.group_by = [...] mais cela casserait la sémantique de la requête et ne fonctionnerait pas comme prévu.
Luis Masuelli
-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

vous devez d'abord importer Sum puis ..

Chaîne YouTube Kiran S
la source