Wie kann man als GROUP BY in Django abfragen?


332

Ich frage ein Modell ab:

Members.objects.all()

Und es kehrt zurück:

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

Ich möchte wissen group_by, wie Django eine Abfrage in meiner Datenbank am besten auslösen kann, z. B.:

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

Was natürlich nicht funktioniert. Ich weiß, dass wir ein paar Tricks machen können django/db/models/query.py, aber ich bin nur neugierig, wie es geht, ohne zu patchen.

Antworten:


483

Wenn Sie eine Aggregation durchführen möchten, können Sie die Aggregationsfunktionen des ORM verwenden :

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

Dies führt zu einer Abfrage ähnlich wie

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

und die Ausgabe würde von der Form sein

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]

6
@ Harry: Du kannst es verketten. So etwas wie:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli

57
Ich habe eine Frage, diese Abfrage gibt nur Bezeichnung und Anzahl zurück. Was ist, wenn ich auch andere Werte der Tabelle erhalten möchte?
AJ

19
Beachten Sie, dass Ihre Sortierung, wenn sie ein anderes Feld als die Bezeichnung ist, ohne Zurücksetzen der Sortierung nicht funktioniert. Siehe stackoverflow.com/a/1341667/202137
Gidgidonihah

12
@ Gidgidonihah Richtig, das Beispiel sollte lautenMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix

7
Ich habe eine Frage, diese Abfrage gibt nur Bezeichnung und Anzahl zurück. Was ist, wenn ich auch andere Werte der Tabelle erhalten möchte?
Yann 叶

55

Eine einfache Lösung, aber nicht die richtige, ist die Verwendung von Raw SQL :

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

Eine andere Lösung besteht darin, die group_byEigenschaft zu verwenden:

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

Sie können jetzt die Ergebnisvariable durchlaufen, um Ihre Ergebnisse abzurufen. Beachten Sie, dass dies group_bynicht dokumentiert ist und in zukünftigen Versionen von Django geändert werden kann.

Und ... warum willst du verwenden group_by? Wenn Sie keine Aggregation verwenden, können Sie order_byein gleiches Ergebnis erzielen.


Kannst du mir bitte sagen, wie es mit order_by geht?
simplyharsh

2
Hallo, wenn Sie keine Aggregation verwenden, können Sie group_by mithilfe von order_by emulieren und die nicht benötigten Einträge entfernen. Dies ist natürlich eine Emulation und kann nur verwendet werden, wenn nicht viele Daten verwendet werden. Da er nicht von Aggregation sprach, dachte ich, dass es eine Lösung sein könnte.
Michael

Hey, das ist großartig - können Sie bitte erklären, wie die Verwendung von execute_sql nicht zu funktionieren scheint ..
rh0dium

8
Beachten Sie, dass dies unter Django 1.9 nicht mehr funktioniert. stackoverflow.com/questions/35558120/…
Grokpot

1
Dies ist eine Art Hack-ish-Methode, um das ORM zu verwenden. Sie sollten keine neuen Abfragesätze instanziieren müssen, die alte manuell übergeben.
Ian Kirkpatrick

32

Sie können das regroupVorlagen-Tag auch zum Gruppieren nach Attributen verwenden. Aus den Dokumenten:

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>

Sieht aus wie das:

  • Indien
    • Mumbai: 19.000.000
    • Kalkutta: 15.000.000
  • USA
    • New York: 20.000.000
    • Chicago: 7.000.000
  • Japan
    • Tokio: 33.000.000

Es funktioniert auch weiter QuerySet s, glaube ich.

Quelle: https://docs.djangoproject.com/de/2.1/ref/templates/builtins/#regroup

Bearbeiten: Beachten Sie, dass das regroupTag nicht wie erwartet funktioniert, wenn Ihre Wörterbuchliste nicht nach Schlüsseln sortiert ist. Es funktioniert iterativ. Sortieren Sie Ihre Liste (oder Ihren Abfragesatz) nach dem Schlüssel des Zackenbarschs, bevor Sie sie an das regroupTag übergeben.


1
Dies ist perfekt! Ich habe viel nach einem einfachen Weg gesucht, dies zu tun. Und es funktioniert auch auf Abfragesätzen, so habe ich es benutzt.
CarmenA

1
Dies ist völlig falsch, wenn Sie große Datenmengen aus der Datenbank lesen und dann nur aggregierte Werte verwenden.
Sławomir Lenart

@ SławomirLenart sicher, dies ist möglicherweise nicht so effizient wie eine reine DB-Abfrage. Aber für einfache Anwendungsfälle kann es eine schöne Lösung sein
Inostia

Dies funktioniert, wenn das Ergebnis in der Vorlage angezeigt wird. Aber für JsonResponse oder andere indirekte Antworten. Diese Lösung wird nicht funktionieren.
Willy Satrio Nugroho

1
@Willysatrionugroho, wenn Sie es in einer Ansicht tun wollten, könnte beispielsweise stackoverflow.com/questions/477820/… für Sie arbeiten
inostia

7

Sie müssen benutzerdefiniertes SQL ausführen, wie in diesem Snippet veranschaulicht:

Benutzerdefiniertes SQL über Unterabfrage

Oder in einem benutzerdefinierten Manager, wie in den Online-Django-Dokumenten gezeigt:

Hinzufügen zusätzlicher Manager-Methoden


1
Art von Roundtrip-Lösung. Ich hätte es benutzt, wenn ich es länger benutzt hätte. Aber hier brauche ich nur die Anzahl der Mitglieder pro Bezeichnung, das ist alles.
simplyharsh

Kein Problem. Ich dachte darüber nach, 1.1 Aggregationsfunktionen zu erwähnen, ging aber davon aus, dass Sie die Release-Version verwenden :)
Van Gale

Es geht darum, rohe Abfragen zu verwenden, die die Schwäche von Djangos ORM zeigen.
Sławomir Lenart

5

Django unterstützt keine kostenlose Gruppierung nach Abfragen . Ich habe es sehr schlecht gelernt. ORM ist nicht dafür ausgelegt, Dinge wie das zu unterstützen, was Sie tun möchten, ohne benutzerdefiniertes SQL zu verwenden. Sie sind beschränkt auf:

  • RAW-SQL (dh MyModel.objects.raw ())
  • cr.execute Sätze (und eine handgemachte Analyse des Ergebnisses).
  • .annotate() (Die Gruppierung nach Sätzen wird im untergeordneten Modell für .annotate () ausgeführt, in Beispielen wie dem Aggregieren von lines_count = Count ('lines'))).

Über ein Queryset können qsSie aufrufen, qs.query.group_by = ['field1', 'field2', ...]aber es ist riskant, wenn Sie nicht wissen, welche Abfrage Sie bearbeiten, und keine Garantie dafür haben, dass sie funktioniert und keine Interna des QuerySet-Objekts beschädigt. Außerdem handelt es sich um eine interne (undokumentierte) API, auf die Sie nicht direkt zugreifen sollten, ohne zu riskieren, dass der Code nicht mehr mit zukünftigen Django-Versionen kompatibel ist.


In der Tat sind Sie nicht nur in der freien Gruppe beschränkt, sondern versuchen Sie es mit SQLAlchemy anstelle von Django ORM.
Sławomir Lenart

5

Es gibt ein Modul, mit dem Sie Django-Modelle gruppieren und dennoch mit einem QuerySet im Ergebnis arbeiten können: https://github.com/kako-nawao/django-group-by

Zum Beispiel:

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>

Der Unterschied zu den annotate/ aggregatebasic Django-Abfragen besteht in der Verwendung der Attribute eines verwandten Feldes, z book.author.last_name.

Wenn Sie die PKs der Instanzen benötigen, die zusammen gruppiert wurden, fügen Sie die folgende Anmerkung hinzu:

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

HINWEIS: ArrayAggist eine Postgres-spezifische Funktion, die ab Django 1.9 verfügbar ist: https://docs.djangoproject.com/de/1.10/ref/contrib/postgres/aggregates/#arrayagg


Diese Django-Gruppierung ist eine Alternative zur valuesMethode. Es ist für einen anderen Zweck, denke ich.
LShi

1
@ LShi Es ist natürlich keine Alternative zu Werten. valuesist ein SQL, selectwährend group_byes ein SQL ist group by(wie der Name schon sagt ...). Warum das Downvote? Wir verwenden solchen Code in der Produktion, um komplexe group_byAnweisungen zu implementieren .
Risadinha

Das Dokument sagt group_by"verhält sich hauptsächlich wie die Wertemethode, aber mit einem Unterschied ..." Das Dokument erwähnt SQL nicht GROUP BYund der Anwendungsfall, den es bereitstellt, deutet nicht darauf hin, dass es etwas mit SQL zu tun hat GROUP BY. Ich werde die Abwahl zurückziehen, wenn jemand dies klargestellt hat, aber dieses Dokument ist wirklich irreführend.
LShi

Nachdem ich das Dokument fürvalues gelesen hatte , stellte ich fest, dass ich selbst vermisst habe, dass es valueswie ein GROUP BY funktioniert. Es ist meine Schuld. Ich denke, es ist einfacher zu verwenden itertools.groupbyals diese Django-Gruppierung, wenn sie valuesnicht ausreicht.
LShi

1
Es ist unmöglich, dies group byvon oben mit einem einfachen valuesAufruf zu tun - mit oder ohne annotateund ohne alles aus der Datenbank abzurufen. Ihr Vorschlag itertools.groupbyfunktioniert für kleine Datensätze, jedoch nicht für mehrere Tausend Datensätze, die Sie wahrscheinlich paginieren möchten. An diesem Punkt müssen Sie natürlich über einen speziellen Suchindex nachdenken, der ohnehin vorbereitete (bereits gruppierte) Daten enthält.
Risadinha

0

Das Dokument besagt, dass Sie Werte verwenden können, um das Abfrageset zu gruppieren.

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

Mit diesem Code können Sie alle Bücher finden und nach Namen gruppieren:

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

Sie können einige cheet Blatt sehen hier .


-1

Wenn ich mich nicht irre, können Sie verwenden, was auch immer-Abfragesatz .group_by = [' Feld ']


8
Dies ist zumindest in Django 1.6 nicht der Fall: Das Objekt 'QuerySet' hat kein Attribut 'group_by'
Facundo Olano

1
Eine ordnungsgemäße Verwendung könnte queryset.query.group_by = [...] sein, dies würde jedoch die Semantik der Abfrage beeinträchtigen und nicht wie erwartet funktionieren.
Luis Masuelli

-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

Zuerst müssen Sie Sum importieren, dann ..

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.