Django: згрупувати за датою (день, місяць, рік)


89

У мене є така проста модель:

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

І я хочу вивести розподіл по місяцях:

  • Скільки продажів було за місяць ( COUNT)
  • Сукупне значення ( SUM)

Я не впевнений, що найкращий спосіб атакувати це. Я бачив деякі досить страшні на вигляд запити додаткового відбору, але мій простий розум каже мені, що мені може бути краще просто повторювати числа, починаючи з довільного старту року / місяця і відлічуючи, поки не досягну поточного місяця, викидаючи прості фільтрація запитів за цей місяць. Більше роботи з базами даних - менше стресу для розробників!

Що для вас найбільше сенсу? Чи є приємний спосіб я повернути швидку таблицю даних? Або мій брудний метод, мабуть, найкраща ідея?

Я використовую Django 1.3. Не впевнений, чи GROUP_BYнещодавно вони додали приємніший спосіб .


Відповіді:


219

Django 1.10 і вище

Невдовзі документація Django перерахована extraяк застаріла . (Дякую, що вказали на це @seddonym, @ Lucas03). Я відкрив квиток, і це рішення, яке надало 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 

Старіші версії

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

Правки

  • Додана кількість
  • Додана інформація для django> = 1.10

1
який серверний сервер бази даних ви використовуєте - він чудово працює в 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 Фіксовані (Завдяки jarshwah)
tback

1
Truncmonth недоступний у Django 1.8
Sudhakaran Packianathan

2
дякую, чудово працює. наріжний футляр для версії до 1.10: якщо хтось приєднується / фільтрує інші моделі, які можуть мати те саме поле (наприклад: позначка часу), тоді потрібно повністю визначити поле -'{}.timestamp'.format(model._meta.db_table)
zsepi

1
Просто коротко зауважимо, що якщо USE_TZвстановлено налаштування Django True, ці дві версії не є абсолютно еквівалентними. Версія, яка використовує TruncMonth, перетворить часову позначку в часовий пояс, заданий TIME_ZONEпараметром, перед усіченням, тоді як версія, що використовує date_trunc_sql, скоротить необроблену часову позначку UTC у базі даних.
Деніел Хардінг,

32

Просто невелике доповнення до відповіді @tback: у мене це не працювало з Django 1.10.6 та postgres. Я додав order_by () наприкінці, щоб це виправити.

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
так: docs.djangoproject.com/en/1.11/topics/db/aggregation/… ... не схоже на хороший дизайн, але вони дуже розумні, ці хлопці-джанго, так це насправді.
Вільямс,

TruncDateдозволяє групуватися за датою (днем місяця)
Ніл

10

Інший підхід - використання ExtractMonth. Я зіткнувся з проблемами при використанні TruncMonth через повернення лише одного значення року та дати. Наприклад, поверталися лише місяці 2009 року. ExtractMonth відмінно вирішив цю проблему і може бути використаний, як показано нижче:

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

Це querysetсписок Замовлення, один рядок на місяць, що поєднує суму продажів:sales_sum

@Django 2.1.7


1

Ось мій брудний метод. Це брудно.

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

Можливо, є кращий спосіб циклу років / місяців, але це насправді не те, що мене цікавить :)


BTW Це буде працювати нормально, але ви знаєте, цикл протягом місяців також не є чудовою ідеєю. Що, якщо хтось захоче зробити це в день місяця, тоді цей цикл буде повторюватися через 30-31 день. в іншому випадку він працює нормально
Mayank Pratap Singh

це занадто повільно, якщо у вас є мільйони записів
інший

@jifferent Абсолютно! Я додав його, щоб показати, яким було моє рішення на момент публікації запитання. Інші відповіді набагато кращі.
Олі

0

Ось як можна згрупувати дані за довільними періодами часу:

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

По місяцях:

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

За роками:

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

Протягом дня:

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

Не забудьте імпортувати Count

from django.db.models import Count

Для django <1.10


3
Так, чудова практика, імпортувати все з моделей
JC Rocamonde

Я явно іронізував. Це робити жахливо. Ви не повинні цього робити, і я б проголосував лише за це (чого я не зробив)
JC Rocamonde,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.