Django: Grouper par date (jour, mois, année)

89

J'ai un modèle simple comme celui-ci:

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

Et je veux produire une ventilation mois par mois de:

  • Combien de ventes il y a eu en un mois ( COUNT)
  • La valeur combinée ( SUM)

Je ne sais pas quelle est la meilleure façon d'attaquer cela. J'ai vu des requêtes extra-sélectives assez effrayantes, mais mon esprit simple me dit que je ferais peut-être mieux d'itérer des nombres, en commençant par une année / mois de début arbitraire et en comptant jusqu'à ce que j'atteigne le mois en cours, en jetant simplement filtrage des requêtes pour ce mois. Plus de travail sur la base de données - moins de stress pour les développeurs!

Qu'est-ce qui a le plus de sens pour vous? Existe-t-il une bonne façon de récupérer un tableau rapide de données? Ou est-ce que ma méthode sale est probablement la meilleure idée?

J'utilise Django 1.3. Je ne sais pas s'ils ont ajouté une manière plus agréable GROUP_BYrécemment.

Oli
la source

Réponses:

219

Django 1.10 et supérieur

Listes de documentation Django extracomme bientôt plus . (Merci de l'avoir signalé @seddonym, @ Lucas03). J'ai ouvert un ticket et c'est la solution fournie par 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 

Versions plus anciennes

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

Modifications

  • Compte ajouté
  • Ajout d'informations pour django> = 1.10
tback
la source
1
quel backend de base de données utilisez-vous - cela fonctionne bien dans 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 (Merci à jarshwah)
retour
1
Truncmonth n'est pas disponible dans Django 1.8
Sudhakaran Packianathan
2
merci, fonctionne très bien. cas d'angle pour la version pré-1.10: si l'on rejoint / filtre sur d'autres modèles qui pourraient avoir le même champ (ex: horodatage), alors il faut qualifier complètement le champ -'{}.timestamp'.format(model._meta.db_table)
zsepi
1
Juste une petite note que si le USE_TZparamètre Django est True, les deux versions ne sont pas exactement équivalentes. La version utilisant TruncMonthconvertira l'horodatage dans le fuseau horaire spécifié par le TIME_ZONEparamètre avant de tronquer, tandis que la version utilisant date_trunc_sqltronquera l'horodatage UTC brut dans la base de données.
Daniel Harding
32

Juste un petit ajout à la réponse @tback: cela n'a pas fonctionné pour moi avec Django 1.10.6 et postgres. J'ai ajouté order_by () à la fin pour le réparer.

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
la source
1
yup: docs.djangoproject.com/en/1.11/topics/db/aggregation/ ... ... ne se sent pas comme un bon design mais ils sont très très intelligents ces gars de Django, donc c'est en fait.
Williams le
TruncDatevous permet de regrouper par date (jour du mois)
Neil
10

Une autre approche consiste à utiliser ExtractMonth. J'ai rencontré des problèmes lors de l'utilisation de TruncMonth en raison du retour d'une seule valeur d'année datetime. Par exemple, seuls les mois de 2009 ont été renvoyés. ExtractMonth a parfaitement résolu ce problème et peut être utilisé comme ci-dessous:

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

Il querysets'agit d'une liste de commandes, une ligne par mois, combinant la somme des ventes:sales_sum

@Django 2.1.7

CK
la source
1

Voici ma sale méthode. C'est sale.

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

Il y a peut-être un meilleur moyen de boucler des années / mois mais ce n'est pas vraiment ce qui m'importe :)

Oli
la source
BTW Cela fonctionnera bien, mais vous savez que la boucle sur des mois n'est pas non plus une bonne idée. Que faire si quelqu'un veut le faire en jour d'un mois, cette boucle sera itérée de 30 à 31 jours. sinon
ça
c'est trop lent si vous avez des millions d'enregistrements
jifferent
@jifferent Absolument! Je l'ai ajouté pour montrer quelle était ma solution au moment de la publication de la question. Les autres réponses sont bien meilleures.
Oli
0

Voici comment regrouper les données par périodes de temps arbitraires:

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
la source
-1

Par mois:

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

Par année:

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

De jour:

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

N'oubliez pas d'importer Count

from django.db.models import Count

Pour django <1.10

jatin
la source
3
Ouais, bonne pratique, importez tout à partir de modèles
JC Rocamonde
J'étais clairement ironique. C'est une pratique horrible de faire cela. Vous ne devriez pas le faire et j'aurais voté contre pour cela (ce que je n'ai pas fait)
JC Rocamonde