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.