Django: gruppo per data (giorno, mese, anno)


89

Ho un modello semplice come questo:

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

E voglio produrre una ripartizione mese per mese di:

  • Quante vendite ci sono state in un mese ( COUNT)
  • Il valore combinato ( SUM)

Non sono sicuro di quale sia il modo migliore per attaccare questo. Ho visto alcune query extra-selezionate dall'aspetto abbastanza spaventoso, ma la mia mente semplice mi dice che potrei stare meglio solo ripetendo numeri, a partire da un anno / mese di inizio arbitrario e contando fino a raggiungere il mese corrente, buttando fuori semplici query che filtrano per quel mese. Più lavoro sul database - meno stress per gli sviluppatori!

Cosa ha più senso per te? C'è un modo carino per recuperare una rapida tabella di dati? O il mio metodo sporco è probabilmente l'idea migliore?

Sto usando Django 1.3. Non sono sicuro che abbiano aggiunto un modo migliore a GROUP_BYrecentemente.


Risposte:


219

Django 1.10 e versioni successive

La documentazione di Django elenca extracome deprecata a breve . (Grazie per averlo sottolineato @seddonym, @ Lucas03). Ho aperto un ticket e questa è la soluzione fornita da jarshwah.

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 

Versioni precedenti

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')

Modifiche

  • Conteggio aggiunto
  • Aggiunte informazioni per django> = 1.10

1
quale database backend stai usando - funziona bene 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 Fixed (Thanks to jarshwah)
— tback

1
Truncmonth non è disponibile in Django 1.8
— Sudhakaran Packianathan

2
grazie, funziona benissimo. caso d'angolo per la versione precedente alla 1.10: se ci si unisce / filtra su altri modelli che potrebbero avere lo stesso campo (es: timestamp), allora si deve qualificare completamente il campo -'{}.timestamp'.format(model._meta.db_table)
— zsepi

1
Solo una breve nota che se l' USE_TZimpostazione Django è True, le due versioni non sono esattamente equivalenti. La versione che utilizza TruncMonthconvertirà il timestamp nel fuso orario specificato TIME_ZONEdall'impostazione prima del troncamento, mentre la versione che utilizza date_trunc_sqltroncerà il timestamp UTC grezzo nel database.
— Daniel Harding,

32

Solo una piccola aggiunta alla risposta di @tback: non ha funzionato per me con Django 1.10.6 e postgres. Ho aggiunto order_by () alla fine per risolverlo.

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()

1
yup: docs.djangoproject.com/en/1.11/topics/db/aggregation/… ... non sembra un buon design ma sono molto molto intelligenti quei ragazzi django, quindi in realtà lo è.
— Williams

TruncDateti permette di raggruppare per data (giorno del mese)
— Neil

10

Un altro approccio è usare ExtractMonth. Ho avuto problemi con TruncMonth a causa della restituzione di un solo valore datetime anno. Ad esempio, sono stati restituiti solo i mesi del 2009. ExtractMonth ha risolto perfettamente questo problema e può essere utilizzato come di seguito:

from django.db.models.functions import ExtractMonth
Sales.objects
    .annotate(month=ExtractMonth('timestamp')) 
    .values('month')                          
    .annotate(count=Count('id'))                  
    .values('month', 'count')  

2
    metrics = {
        'sales_sum': Sum('total'),
    }
    queryset = Order.objects.values('created__month')
                               .annotate(**metrics)
                               .order_by('created__month')

Il querysetè una lista di Ordine, una linea al mese, che unisce la somma delle vendite:sales_sum

@Django 2.1.7


1

Ecco il mio metodo sporco. È sporco.

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

Potrebbe esserci un modo migliore di ripetere anni / mesi, ma non è proprio quello che mi interessa :)


A proposito, funzionerà bene ma sai che il loop per mesi non è una grande idea. E se qualcuno volesse farlo nel giorno di un mese, questo ciclo itererà di 30-31 giorni. altrimenti funziona bene
— Mayank Pratap Singh

questo è troppo lento se hai milioni di record
— diverso

@jifferent Assolutamente! L'ho aggiunto per mostrare qual era la mia soluzione al momento della pubblicazione della domanda. Le altre risposte sono molto migliori.
— Oli

0

Ecco come raggruppare i dati in base a periodi di tempo arbitrari:

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))

-1

Per mese:

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

Per anno:

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

Di giorno:

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

Non dimenticare di importare Count

from django.db.models import Count

Per django <1.10


3
Sì, ottima pratica, importa tutto dai modelli
— JC Rocamonde

Chiaramente ero ironico. È una pratica orribile farlo. Non dovresti farlo e avrei svalutato solo per quello (cosa che non ho fatto)
— JC Rocamonde
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.