如何使用Django ORM按用户分组查询指定日期范围的amount累计求和值
问题描述
我有一张财务数据表,字段包含user(用户标识)、amount(金额)、date(记录日期),示例数据如下:
| user | amount | date |
|---|---|---|
| John | 10 | 2017-07-01 |
| John | 20 | 2019-07-01 |
| John | 30 | 2020-09-01 |
| John | 40 | 2021-11-01 |
| ... | ... | ... |
| Dan | -20 | 2019-02-01 |
| Dan | -30 | 2020-04-01 |
| Dan | -40 | 2021-06-01 |
函数入参为日期区间,示例如下:
date_start = '2019-01-01' date_end = '2021-11-07'
预期输出
针对所有用户,返回指定日期区间内每个日期对应的、从最早记录到当前日期的amount累计求和值,效果如下:
| user | amount | date |
|---|---|---|
| John | 30 (10+20) | 2019-07-01 |
| John | 60 (10+20+30) | 2020-09-01 |
| John | 100 (10+20+30+40) | 2021-11-01 |
| ... | ... | ... |
| Dan | -20 | 2019-02-01 |
| Dan | -50 (-20-30) | 2020-04-01 |
| Dan | -90 (-20-30-40) | 2021-06-01 |
现有问题
我编写了如下代码实现该功能:
def get_sum_amount(self, date_start=None, date_end=None): date_detail = {} # 最小日期过滤 if date_start: date_detail['date__gt'] = date_start # 最大日期过滤 if date_end: date_detail['date__lt'] = date_end detail = Financial.objects.filter(Q(**date_detail)) \ .values('user') \ .annotate(sum_amount=Coalesce(Sum(F('amount')), Value(0)))
运行后仅返回了指定日期范围内每个用户的amount总求和值,不符合预期:
| user | amount |
|---|---|
| John | 100 |
| Dan | -90 |
解决方案
原有代码存在两个核心问题:
- 一开始就过滤掉了早于
date_start的记录,导致这部分金额没有被计入累计值 - 只按
user分组聚合,没有保留日期维度,且用普通的Sum只能得到分组总求和,无法实现按日期的滚动累计
修改后的代码如下:
from django.db.models import Sum, F, Window, Q, Coalesce, Value def get_sum_amount(self, date_start=None, date_end=None): # 第一步:先过滤出所有不晚于date_end的记录,早于date_start的要保留参与累计计算 filter_params = Q() if date_end: filter_params &= Q(date__lte=date_end) # 按用户+日期分组聚合,处理同一用户同一天有多条记录的场景 queryset = Financial.objects.filter(filter_params) \ .values('user', 'date') \ .annotate(daily_amount=Coalesce(Sum('amount'), Value(0))) \ .order_by('user', 'date') # 第二步:用窗口函数计算每个用户的累计金额 queryset = queryset.annotate( cumulative_amount=Window( expression=Sum('daily_amount'), partition_by=[F('user')], # 按用户分组计算累计 order_by=[F('date').asc()] # 按日期升序滚动求和 ) ) # 第三步:过滤出日期在指定区间内的结果返回 if date_start: queryset = queryset.filter(date__gte=date_start) # 调整字段名匹配预期输出 return queryset.values('user', amount=F('cumulative_amount'), 'date')
逻辑说明
- 窗口函数
Window的默认计算范围就是「当前分组内从第一条记录到当前行」,正好符合从最早记录到当前日期累计求和的需求 - 先保留所有早于等于
date_end的记录计算累计,最后再过滤出区间内的记录,不会丢失区间外的金额累计 - 提前按用户+日期做每日金额聚合,避免同一天多条数据导致的计算错误
内容的提问来源于stack exchange,提问作者Saeed
相关产品推荐
相关产品推荐

