Django如何通过单查询实现按日期求和并添加7日滚动平均值?
Django日期分组求和+7天滚动平均值实现方案
问题背景
需要对Sales模型执行单次查询,完成两个需求:
- 按日期分组,计算每日
Value字段的总和 - 为每个日期添加**过去7天(含当天)**的滚动平均值
示例数据表:
|---------------------|------------------|--------------| | Date | Category | Value | |---------------------|------------------|--------------| | 2020-04-01 | 1 | 55.0 | |---------------------|------------------|--------------| | 2020-04-01 | 2 | 30.0 | |---------------------|------------------|--------------| | 2020-04-02 | 1 | 25.0 | |---------------------|------------------|--------------| | 2020-04-02 | 2 | 85.0 | |---------------------|------------------|--------------| | 2020-04-03 | 1 | 60.0 | |---------------------|------------------|--------------| | 2020-04-03 | 2 | 30.0 | |---------------------|------------------|--------------|
原尝试代码:
days = ( Sales.objects.values('date').annotate(sum_for_date=Sum('value')) ).annotate( rolling_avg=Window( expression=Avg('sum_for_date'), frame=RowRange(start=-7,end=0), order_by=F('date').asc(), ) ) .order_by('date')
抛出错误:
django.core.exceptions.FieldError: Cannot compute Avg('sum_for_date'): 'sum_for_date' is an aggregate
错误原因
Django的Window函数无法直接引用同一查询中通过annotate生成的聚合字段(如sum_for_date)。这是因为窗口函数的计算逻辑和普通聚合处于查询的不同执行阶段,无法直接复用已生成的聚合结果。
解决方法
方法1:子查询预计算每日总和(适用于连续日期)
先通过子查询得到按日期分组的每日销售额总和,再在外层查询中对该结果应用滚动平均窗口函数:
from django.db.models import Sum, Avg, F, Window from django.db.models.expressions import RowRange # 子查询:计算每日销售额总和 daily_sums = Sales.objects.values('date').annotate(sum_for_date=Sum('value')).order_by('date') # 外层查询:计算7天滚动平均(含当天) result = daily_sums.annotate( rolling_avg=Window( expression=Avg('sum_for_date'), frame=RowRange(start=-6, end=0), # 从当前行往前数6行+当前行,共7行数据 order_by=F('date').asc() ) )
注意:RowRange是按行数计算窗口范围,仅当日期连续无缺失时结果准确。
方法2:基于日期范围的窗口计算(适用于日期有缺失的场景)
如果数据存在日期断层,RowRange会因为行数不匹配导致滚动平均失真,此时应使用RangeFrame结合日期范围定义窗口:
PostgreSQL适配写法
from django.db.models import Sum, Avg, F, Window from django.db.models.expressions import RangeFrame result = Sales.objects.values('date').annotate( sum_for_date=Sum('value') ).annotate( rolling_avg=Window( expression=Avg('sum_for_date'), frame=RangeFrame(start=F('date') - 6, end=F('date')), # 覆盖过去6天到当天的范围 order_by=F('date').asc() ) ).order_by('date')
MySQL适配写法
MySQL不支持直接的日期加减作为RangeFrame参数,需要自定义函数实现日期偏移:
from django.db.models import Sum, Avg, F, Window, Func from django.db.models.expressions import RangeFrame class DateSub6Days(Func): function = 'DATE_SUB' template = "%(function)s(%(expressions)s, INTERVAL 6 DAY)" result = Sales.objects.values('date').annotate( sum_for_date=Sum('value') ).annotate( rolling_avg=Window( expression=Avg('sum_for_date'), frame=RangeFrame(start=DateSub6Days(F('date')), end=F('date')), order_by=F('date').asc() ) ).order_by('date')
内容的提问来源于stack exchange,提问作者hemabe
相关产品推荐
相关产品推荐

