Django: Grupuj według daty (dzień, miesiąc, rok)


94

Mam taki prosty model:

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

Chcę przedstawić podział na miesiąc po miesiącu:

  • Ile sprzedaży było w miesiącu ( COUNT)
  • Połączona wartość ( SUM)

Nie jestem pewien, jaki jest najlepszy sposób na zaatakowanie tego. Widziałem kilka dość przerażająco wyglądających zapytań z dodatkowym wyborem, ale mój prosty umysł podpowiada mi, że może lepiej byłoby po prostu iterować liczby, zaczynając od dowolnego początku roku / miesiąca i licząc aż do osiągnięcia bieżącego miesiąca, wyrzucając proste zapytania filtrujące w tym miesiącu. Więcej pracy z bazą danych - mniej stresu dla programistów!

Co ma dla ciebie największy sens? Czy istnieje dobry sposób na wycofanie szybkiej tabeli danych? A może moja brudna metoda jest prawdopodobnie najlepszym pomysłem?

Używam Django 1.3. Nie jestem pewien, czy ostatnio dodali ładniejszy sposób GROUP_BY.


Odpowiedzi:


225

Django 1.10 i nowsze

Dokumentacja Django wkrótce zostanie uznanaextra za przestarzałą . (Dziękuję za wskazanie tego @seddonym, @ Lucas03). Otworzyłem bilet i to jest rozwiązanie, które zapewniło 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 

starsza wersja

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

Zmiany

  • Dodano liczbę
  • Dodano informacje dla django> = 1.10

1
jakiego zaplecza bazy danych używasz - działa dobrze w 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 Naprawiono (dzięki jarshwah)
cofnięcie z

1
Truncmonth nie jest dostępny w Django 1.8
Sudhakaran Packianathan

2
dzięki, działa świetnie. przypadek narożny dla wersji starszej niż 1.10: jeśli ktoś łączy / filtruje inne modele, które mogą mieć to samo pole (np: znacznik czasu), to należy w pełni zakwalifikować pole -'{}.timestamp'.format(model._meta.db_table)
zsepi

1
Krótka uwaga, że ​​jeśli USE_TZustawienie Django jest takie True, te dwie wersje nie są dokładnie równoważne. Wersja używająca TruncMonthkonwertuje sygnaturę czasową na strefę czasową określoną przez TIME_ZONEustawienie przed obcięciem, podczas gdy wersja używająca date_trunc_sqlobetnie surowy znacznik czasu UTC w bazie danych.
Daniel Harding,

36

Tylko mały dodatek do odpowiedzi @tback: Nie działało to dla mnie z Django 1.10.6 i postgres. Na koniec dodałem order_by (), aby to naprawić.

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/ ... ... nie wygląda na dobry projekt, ale ci goście od django są bardzo sprytni, więc tak naprawdę jest.
Williams

TruncDateumożliwia grupowanie według daty (dnia miesiąca)
Neil

11

Innym podejściem jest użycie ExtractMonth. Podczas korzystania z TruncMonth napotkałem problemy z powodu zwrócenia tylko jednej wartości daty i godziny. Na przykład zwracano tylko miesiące w 2009 roku. ExtractMonth rozwiązał ten problem doskonale i może być używany jak poniżej:

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

To querysetlista Zamówień, jedna linia miesięcznie, łącząca sumę sprzedaży:sales_sum

@Django 2.1.7


1

Oto moja brudna metoda. To jest brudne.

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

Może istnieć lepszy sposób na zapętlanie lat / miesięcy, ale nie na tym mi zależy :)


BTW To będzie działać dobrze, ale wiesz, że pętla przez miesiące również nie jest dobrym pomysłem. A co jeśli ktoś zechce zrobić to w dzień miesiąca to ta pętla będzie iterować co 30-31 dni. w przeciwnym razie działa dobrze
Mayank Pratap Singh

jest to zbyt wolne, jeśli masz miliony rekordów
jifferent

@jifferent Absolutely! Dodałem go, aby pokazać, jakie było moje rozwiązanie w momencie wysyłania pytania. Inne odpowiedzi są znacznie lepsze.
Oli

0

Oto, jak możesz grupować dane według dowolnych okresów:

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

0

mam tabelę zamówień w mojej bazie danych. mam zamiar liczyć zamówienia miesięcznie w ciągu ostatnich 3 miesięcy

from itertools import groupby
from dateutil.relativedelta import relativedelta

date_range = datetime.now()-relativedelta(months=3)
aggs =Orders.objects.filter(created_at=date_range)\
            .extra({'date_created':"date(created_at)"}).values('date_created')

for key , group in groupby(aggs):
     print(key,len(list(group)))

created_at to pole typu data i godzina. dzięki dodatkowej funkcji to, co zrobiono, pobiera datę z wartości datetime. podczas korzystania z daty i godziny możemy nie uzyskać poprawnej liczby, ponieważ obiekty są tworzone o różnych porach dnia.

Pętla for wypisze datę i liczbę zliczeń


-1

Według miesiąca:

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

Według roku:

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

W dzień:

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

Nie zapomnij zaimportować Count

from django.db.models import Count

Dla django <1.10


3
Tak, świetna praktyka, importuj wszystko z modeli
JC Rocamonde,

Najwyraźniej ironizowałem. To okropna praktyka. Nie powinieneś tego robić i zagłosowałbym w dół tylko za to (czego nie zrobiłem)
JC Rocamonde
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.