Django:按月查询组

Posted

技术标签:

【中文标题】Django:按月查询组【英文标题】:Django: Query Group By Month 【发布时间】:2016-10-17 11:46:18 【问题描述】:

如何在不使用额外的情况下按月计算总数?

我目前正在使用:

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's 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: Group by date (day, month, year)


附:我知道这个问题已经被问过多次,但我没有看到任何答案。

谢谢!


解决方案

感谢@Vin-G 的回答。

【问题讨论】:

请展示您为此尝试过的内容 你可以看到这个例子来获得灵感***.com/a/8746532/2581266 @PapoucheGuinslyzinho 我已经测试过了,但我似乎无法导入 TruncMonth 模块。 你有哪个版本的 django? django 1.8 但 tback 说它适用于 1.10 及更高版本。 【参考方案1】:

我不知道我的解决方案是否比你的更快。你应该分析它。尽管如此,我只查询数据库一次而不是 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'

【讨论】:

【参考方案2】:

itertools.groupby 是 Python 中的高性能选项,可用于单个数据库查询:

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

我不知道纯 django ORM 解决方案。 date__month 过滤器非常有限,不能用于valuesorder_by 等。

【讨论】:

在 Python 3.0 中,您可能需要使用 i.date().month。为我工作 Django 1.10 + python 3。 Re:一个纯粹的 Django ORM 解决方案 - 请参阅我下面关于使用本机 datetimes 管理器的帖子,这经常被忽视。【参考方案3】:

首先,您必须创建一个可以为您提取月份的函数:

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

annotate() 和 values() 子句应用于查询的顺序很重要。如果 values() 子句在 annotate() 之前,注释将使用 values() 子句描述的分组计算。

【讨论】:

感谢您的详细回答。我试过你的答案,但月份没有分组。它只是计算每笔交易的总额。 请查看以下链接了解结果。 s32.postimg.org/3ripkuelh/Testdata_result.png 现在终于可以使用了。我只是在最后一个注释之后添加了 .order_by() ,然后它就像魔术一样工作。 :) 谢谢。 您应该直接使用ExtractMonthTruncMonthfrom django.db.models.functions import ExtractMonth, TruncMonth 这肯定是过去的一次爆炸! @msln您最好在python中实现月份数字到月份名称字符串的转换,而不是使用SQL函数,因为数据库服务器之间的日期格式不同。也就是说,如果您坚持在数据库端执行此操作,那么您将函数名称和模板更改为可用于您正在使用的数据库服务器的名称。例如对 postgres 使用 TO_CHAR(value, format) 对 mysql 使用 DATE_FORMAT。见en.wikibooks.org/wiki/SQL_Dialects_Reference/…【参考方案4】:

不要忘记 Django 查询集提供了一个原生的datetimes manager,它可以让您轻松地从任何带有日期时间字段的模型的查询集中提取所有天/周/月/年。因此,如果上面的 Invoice 模型有一个 created 日期时间字段,并且您想要查询集中每个月的总计,您可以这样做:

    invoices = Invoice.objects.all()
    months = invoices.datetimes("created", kind="month")
    for month in months:
        month_invs = invoices.filter(created__month=month.month)
        month_total = month_invs.aggregate(total=Sum("otherfield")).get("total")
        print(f"Month: month, Total: month_total")

不需要外部功能或部门。

【讨论】:

这太棒了!谢谢你。建议:不仅要按月过滤,还要按年过滤,因为 2020-10 和 2021-10 不是同一个月!【参考方案5】:
result = (
    invoices.objects
        .all()
        .values_list('created_at__year', 'created_at__month')
        .annotate(Sum('total'))
        .order_by('created_at__year', 'created_at__month')
)

【讨论】:

不鼓励纯代码回答,当你添加一些上下文来解释这将如何解决问题时,答案值会增加。 比接受的答案简单得多。可以写得更短。不需要 all()

以上是关于Django:按月查询组的主要内容,如果未能解决你的问题,请参考以下文章

Django按月和年过滤

Django按月周天统计数据

优化 django 数据库查询

Django使用日期选择器按月动态搜索项目

按月和日过滤 django 日期时间字段的问题

Django - 返回 Post 对象查询在本地时间跨越到前端的月份列表