Django: группа запросов по месяцам

Как рассчитать общий показатель за месяц, не используя дополнительные?

В настоящее время я использую:

  • django 1.8
  • postgre 9.3.13
  • Python 2.7

Пример.

введите описание изображения здесь

То, что я пробовал до сих пор.

#Doesn't work for me but I don't mind because I don't want to use extra
truncate_month = connection.ops.date_trunc_sql('month','day')
invoices = Invoice.objects.filter(is_deleted = False,company = company).extra({'month': truncate_month}).values('month').annotate(Sum('total'))

----
#It works but I think that it too slow if I query a big set of data
for current_month in range(1,13):
    Invoice.objects.filter(date__month = current__month).annotate(total = Sum("total"))

а также этот, ответ кажется большим, но я не могу импортировать модуль TruncMonth.

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


P.S. Я знаю, что этот вопрос уже задан несколько раз, но я не вижу никакого ответа.

Спасибо!


Решение

Благодаря ответу @Vin-G.

введите описание изображения здесь

Ответ 1

Сначала вы должны создать функцию, которая может извлечь месяц для вас:

from django.db import models
from django.db.models import Func

class Month(Func):
    function = 'EXTRACT'
    template = '%(function)s(MONTH from %(expressions)s)'
    output_field = models.IntegerField()

После этого все, что вам нужно сделать, это

  • аннотировать каждую строку с месяцем
  • группируйте результаты в аннотированный месяц, используя values()
  • аннотировать каждый результат с суммарной суммой итогов с помощью Sum()

Важно. Если ваш класс модели имеет порядок по умолчанию, указанный в метафайлах, тогда вам нужно будет добавить пустое предложение order_by(). Это происходит из-за https://docs.djangoproject.com/en/1.9/topics/db/aggregation/#interaction-with-default-ordering-or-order-by

Поля, указанные в части order_by() набора запросов (или используемые при упорядочении по умолчанию для модели), используются при выборе выходных данных, даже если они не указаны иначе в вызове values(), Эти дополнительные поля используются для группирования "похожих" результатов вместе, и они могут сделать иначе идентичные строки результатов, как представляется, отдельными.

Если вы не уверены, вы можете просто добавить пустое предложение order_by() в любом случае без каких-либо неблагоприятных последствий.

то есть.

from django.db.models import Sum

summary = (Invoice.objects
              .annotate(m=Month('date'))
              .values('m')
              .annotate(total=Sum('total'))
              .order_by())

См. полный текст здесь: https://gist.github.com/alvingonzales/ff9333e39d221981e5fc4cd6cdafdd17

Если вам нужна дополнительная информация:

Подробная информация о создании собственных классов Func: https://docs.djangoproject.com/en/1.8/ref/models/expressions/#func-expressions

Подробности о предложении values ​​() (обратите внимание на то, как он взаимодействует с annotate() в отношении порядка предложений): https://docs.djangoproject.com/en/1.9/topics/db/aggregation/#values

порядок, в котором условия аннотации() и values ​​() применяются к запросу, является значительным. Если предложение values ​​() предшествует аннотации(), аннотация будет вычисляться с использованием группировки, описываемой предложением values ​​().

Ответ 2

itertools.groupby является опцией исполнения в Python и может использоваться с одним запросом db:

from itertools import groupby

invoices = Invoice.objects.only('date', 'total').order_by('date')
month_totals = {
    k: sum(x.total for x in g) 
    for k, g in groupby(invoices, key=lambda i: i.date.month)
}
month_totals
# {1: 100, 3: 100, 4: 500, 7: 500}

Мне не известно о чистом решении ORM для Django. Фильтр date__month очень ограничен и не может использоваться в values, order_by и т.д.

Ответ 3

Я не знаю, будет ли мое решение быстрее вашего. Вы должны просмотреть профиль. Тем не менее я только запрашиваю db один раз вместо 12 раз.

#utils.py
from django.db.models import Count, Sum


def get_total_per_month_value():
    """
    Return the total of sales per month

    ReturnType: [Dict]
    {'December': 3400, 'February': 224, 'January': 792}
    """
    result= {}
    db_result = Sale.objects.values('price','created')
    for i in db_result:
        month = str(i.get('created').strftime("%B"))
        if month in result.keys():
            result[month] = result[month] + i.get('price')
        else:
            result[month] = i.get('price')
    return result

#models.py
class Sale(models.Model):
    price = models.PositiveSmallIntegerField()
    created = models.DateTimeField(_(u'Published'), default="2001-02-24")

#views.py
from .utils import get_total_per_month_value
# ...
result = get_total_per_month_value()

test.py

  #
    import pytest
    from mixer.backend.django import mixer
    #Don't try to write in the database
    pytestmark = pytest.mark.django_db
    def test_get_total_per_month():
        from .utils import get_total_per_month_value
        selected_date = ['01','02','03','01','01']
        #2016-01-12 == YYYY-MM-DD
        for i in selected_date:
            mixer.blend('myapp.Sale', created="2016-"+i+"-12")
        values = get_total_per_month_value() #return a dict
        months = values.keys()
        assert 'January' in months, 'Should include January'
        assert 'February' in months, 'Should include February'
        assert len(months) == 3, 'Should aggregate the months'