Django: Gruppe nach Datum (Tag, Monat, Jahr)

89

Ich habe ein einfaches Modell wie dieses:

class Order(models.Model):
    created = model.DateTimeField(auto_now_add=True)
    total = models.IntegerField() # monetary value

Und ich möchte eine monatliche Aufschlüsselung von:

  • Wie viele Verkäufe gab es in einem Monat ( COUNT)
  • Der kombinierte Wert ( SUM)

Ich bin mir nicht sicher, wie ich das am besten angreifen kann. Ich habe einige ziemlich beängstigend aussehende Extra-Select-Abfragen gesehen, aber mein einfacher Verstand sagt mir, dass ich vielleicht besser dran bin, nur Zahlen zu iterieren, beginnend mit einem willkürlichen Startjahr / Monat und bis zum Erreichen des aktuellen Monats zu zählen und einfach wegzuwerfen Abfragen, die für diesen Monat gefiltert werden. Mehr Datenbankarbeit - weniger Entwicklerstress!

Was macht für Sie am meisten Sinn? Gibt es eine gute Möglichkeit, eine schnelle Datentabelle zurückzuziehen? Oder ist meine schmutzige Methode wahrscheinlich die beste Idee?

Ich benutze Django 1.3. Ich bin mir nicht sicher, ob sie in GROUP_BYletzter Zeit einen schöneren Weg hinzugefügt haben .

Oli
quelle

Antworten:

219

Django 1.10 und höher

Die Django-Dokumentation wird baldextra als veraltet eingestuft . (Danke, dass Sie auf @seddonym, @ Lucas03 hingewiesen haben). Ich habe ein Ticket geöffnet und dies ist die Lösung, die Jarshwah bereitgestellt hat.

from django.db.models.functions import TruncMonth
from django.db.models import Count

Sales.objects
    .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
    .values('month')                          # Group By month
    .annotate(c=Count('id'))                  # Select the count of the grouping
    .values('month', 'c')                     # (might be redundant, haven't tested) select month and count 

ältere Versionen

from django.db import connection
from django.db.models import Sum, Count

truncate_date = connection.ops.date_trunc_sql('month', 'created')
qs = Order.objects.extra({'month':truncate_date})
report = qs.values('month').annotate(Sum('total'), Count('pk')).order_by('month')

Bearbeitungen

  • Anzahl hinzugefügt
  • Informationen für django> = 1.10 hinzugefügt
tback
quelle
1
Welches Datenbank-Backend verwenden Sie - es funktioniert gut in Postgres>>> qs.extra({'month':td}).values('month').annotate(Sum('total')) [{'total__sum': Decimal('1234.56'), 'month': datetime.datetime(2011, 12, 1, 0, 0)}]
Tback
1
@seddonym Behoben (Dank an jarshwah)
tback
1
Truncmonth ist nicht verfügbar in Django 1.8
Sudhakaran Packianathan
2
Danke, funktioniert super. Eckfall für die Version vor 1.10: Wenn man andere Modelle verbindet / filtert, die möglicherweise dasselbe Feld haben (z. B. Zeitstempel), muss man das Feld vollständig qualifizieren -'{}.timestamp'.format(model._meta.db_table)
zsepi
1
Nur eine kurze Anmerkung: Wenn die Django- USE_TZEinstellung aktiviert ist True, sind die beiden Versionen nicht genau gleichwertig. Die verwendete Version TruncMonthkonvertiert den Zeitstempel TIME_ZONEvor dem Abschneiden in die durch die Einstellung angegebene Zeitzone , während die verwendete Version date_trunc_sqlden unformatierten UTC-Zeitstempel in der Datenbank abschneidet.
Daniel Harding
29

Nur eine kleine Ergänzung zu @tback Antwort: Es hat bei mir mit Django 1.10.6 und Postgres nicht funktioniert. Ich habe am Ende order_by () hinzugefügt, um das Problem zu beheben.

from django.db.models.functions import TruncMonth
Sales.objects
    .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
    .values('month')                          # Group By month
    .annotate(c=Count('id'))                  # Select the count of the grouping
    .order_by()
Rani
quelle
1
yup: docs.djangoproject.com/de/1.11/topics/db/aggregation/… ... fühlt sich nicht nach gutem Design an, aber sie sind sehr, sehr schlau, diese Django-Typen, also ist es tatsächlich so.
Williams
TruncDateErmöglicht die Gruppierung nach Datum (Tag des Monats)
Neil
10

Ein anderer Ansatz ist zu verwenden ExtractMonth. Bei der Verwendung von TruncMonth sind Probleme aufgetreten, da nur ein Datum / Uhrzeit-Jahreswert zurückgegeben wurde. Beispielsweise wurden nur die Monate 2009 zurückgegeben. ExtractMonth hat dieses Problem perfekt behoben und kann wie folgt verwendet werden:

from django.db.models.functions import ExtractMonth
Sales.objects
    .annotate(month=ExtractMonth('timestamp')) 
    .values('month')                          
    .annotate(count=Count('id'))                  
    .values('month', 'count')  
Schildkröte
quelle
2
    metrics = {
        'sales_sum': Sum('total'),
    }
    queryset = Order.objects.values('created__month')
                               .annotate(**metrics)
                               .order_by('created__month')

Dies querysetist eine Liste der Bestellungen, eine Zeile pro Monat, die die Summe der Verkäufe kombiniert:sales_sum

@ Django 2.1.7

CK
quelle
1

Hier ist meine schmutzige Methode. Es ist schmutzig.

import datetime, decimal
from django.db.models import Count, Sum
from account.models import Order
d = []

# arbitrary starting dates
year = 2011
month = 12

cyear = datetime.date.today().year
cmonth = datetime.date.today().month

while year <= cyear:
    while (year < cyear and month <= 12) or (year == cyear and month <= cmonth):
        sales = Order.objects.filter(created__year=year, created__month=month).aggregate(Count('total'), Sum('total'))
        d.append({
            'year': year,
            'month': month,
            'sales': sales['total__count'] or 0,
            'value': decimal.Decimal(sales['total__sum'] or 0),
        })
        month += 1
    month = 1
    year += 1

Es gibt vielleicht eine bessere Möglichkeit, Jahre / Monate zu wiederholen, aber das interessiert mich nicht wirklich :)

Oli
quelle
Übrigens wird es gut funktionieren, aber Sie wissen, Schleife über Monate auch keine gute Idee. Was ist, wenn jemand es am Tag eines Monats schaffen möchte, dann wird diese Schleife um 30-31 Tage wiederholt. ansonsten funktioniert es gut
Mayank Pratap Singh
Dies ist zu langsam, wenn Sie Millionen von Datensätzen haben
anders
@jifferent Absolut! Ich habe es hinzugefügt, um zu zeigen, was meine Lösung zum Zeitpunkt der Veröffentlichung der Frage war. Die anderen Antworten sind viel besser.
Oli
0

So können Sie Daten nach beliebigen Zeiträumen gruppieren:

from django.db.models import F, Sum
from django.db.models.functions import Extract, Cast
period_length = 60*15 # 15 minutes

# Annotate each order with a "period"
qs = Order.objects.annotate(
    timestamp=Cast(Extract('date', 'epoch'), models.IntegerField()),
    period=(F('timestamp') / period_length) * period_length,
)

# Group orders by period & calculate sum of totals for each period
qs.values('period').annotate(total=Sum(field))
Max Malysh
quelle
-1

Nach Monaten:

 Order.objects.filter().extra({'month':"Extract(month from created)"}).values_list('month').annotate(Count('id'))

Pro Jahr:

 Order.objects.filter().extra({'year':"Extract(year from created)"}).values_list('year').annotate(Count('id'))

Tagsüber:

 Order.objects.filter().extra({'day':"Extract(day from created)"}).values_list('day').annotate(Count('id'))

Vergessen Sie nicht, Count zu importieren

from django.db.models import Count

Für Django <1.10

Jatin
quelle
3
Ja, großartige Übung, importiere alles von Modellen
JC Rocamonde
Ich war eindeutig ironisch. Es ist eine schreckliche Praxis, das zu tun. Sie sollten es nicht tun und ich hätte nur dafür herabgestimmt (was ich nicht getan habe)
JC Rocamonde