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'