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.