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


94

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

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

И я хочу вывести разбивку по месяцам:

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

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

Что для вас наиболее важно? Есть ли хороший способ вернуть быструю таблицу данных? Или мой грязный метод, вероятно, лучшая идея?

Я использую Django 1.3. Не уверен, что GROUP_BYнедавно они добавили более приятный способ .


Ответы:


225

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 05

1
@seddonym Fixed (Спасибо jarshwah)
tback

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

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

1
Замечу, что если параметр Django USE_TZустановлен True, две версии не совсем эквивалентны. Версия, использующая TruncMonth, преобразует метку времени в часовой пояс, указанный в TIME_ZONEнастройке, перед усечением, в то время как версия, использующая, date_trunc_sqlбудет усекать необработанную метку времени UTC в базе данных.
Дэниел Хардинг

36

Небольшое дополнение к ответу @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/… ... не кажется хорошим дизайном, но они очень умны, ребята из django, так что на самом деле так оно и есть.
Williams

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

11

Другой подход - использовать ExtractMonth. У меня возникли проблемы с использованием TruncMonth из-за того, что возвращалось только одно значение года datetime. Например, возвращались только месяцы 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

Возможно, есть лучший способ зацикливать годы / месяцы, но это не совсем то, что меня волнует :)


Кстати, это будет работать нормально, но вы знаете, что цикл на несколько месяцев тоже не лучшая идея. Что, если кто-то захочет сделать это в День месяца, тогда этот цикл будет повторяться 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))

0

у меня есть таблица заказов в моей базе данных. я буду считать заказы в месяц за последние 3 месяца

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 - поле даты и времени. с помощью дополнительной функции то, что сделано, берет дату из значений datetime. при использовании datetime мы можем не получить правильный счет, потому что объекты создаются в разное время дня.

Цикл for напечатает дату и количество отсчетов.


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

Не забудьте импортировать счетчик

from django.db.models import Count

Для django <1.10


3
Да, отличная практика, импорт всего от моделей
JC

Я явно иронизировал. Это ужасная практика. Вы не должны этого делать, и я бы проголосовал против только за это (чего я не делал)
JC
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.