Django Annotation分组结果不符合预期问题求助
解决Django按月分组统计各状态项目数量的问题
问题背景
我的Django模型定义如下:
class Project(models.Model): name = models.CharField(max_length=1000, null=True, blank=True) date_created = models.DateTimeField(auto_now=True) status = models.CharField(max_length=1000, null=True, blank=True)
status字段包含Won、Lost、Open、Pending、Cancelled等取值,需要查询指定时间范围内每月各状态的项目数量。
尝试了以下代码,但存在两个核心问题:
- 同一月份会生成多个对象,每种有项目的状态对应一个独立对象,分组逻辑异常;
- 状态取值为硬编码,新增状态时必须手动修改代码。
尝试的代码:
queryset = list(opps.annotate( month=TruncMonth('date_created'), ).values('month').annotate( total=Count('id'), Win=Count('id', filter=Q(status='Win')), Loss=Count('id', filter=Q(status='Loss')), Open=Count('id', filter=Q(status='Open')), Dormant=Count('id', filter=Q(status='Dormant')), Pending=Count('id', filter=Q(status='Pending')), Cancelled=Count('id', filter=Q(status='Cancelled')), ))
当前错误输出示例:
[{'month': datetime.datetime(2022, 5, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 1, 'Win': 0, 'Loss': 1, 'Open': 0, 'Dormant': 0, 'Pending': 0, 'Cancelled': 0}, {'month': datetime.datetime(2022, 5, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 1, 'Win': 0, 'Loss': 1, 'Open': 0, 'Dormant': 0, 'Pending': 0, 'Cancelled': 0}, {'month': datetime.datetime(2022, 5, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 1, 'Win': 0, 'Loss': 0, 'Open': 1, 'Dormant': 0, 'Pending': 0, 'Cancelled': 0}, {'month': datetime.datetime(2022, 6, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 1, 'Win': 0, 'Loss': 0, 'Open': 1, 'Dormant': 0, 'Pending': 0, 'Cancelled': 0}]
解决方案
1. 修复分组异常
分组异常的核心原因是查询逻辑中,values('month')的分组范围未被正确限定。正确流程是:先过滤时间范围,再截断月份,最后仅按month字段进行聚合分组,避免多余的分组维度。
2. 解决硬编码状态问题
动态获取指定时间范围内所有存在的status取值,再通过动态参数生成每个状态的聚合注解,无需硬编码状态值。
完整代码示例
from django.db.models import Count, Q, TruncMonth from django.utils import timezone # 1. 定义查询的时间范围(示例为过去一年) start_date = timezone.now() - timezone.timedelta(days=365) end_date = timezone.now() # 2. 动态获取时间范围内所有非空的status取值(去重) all_statuses = Project.objects.filter( date_created__range=(start_date, end_date) ).values_list('status', flat=True).distinct() all_statuses = [status for status in all_statuses if status] # 3. 构建基础查询:过滤时间范围 -> 截断月份 -> 按月份分组并统计总数 queryset = Project.objects.filter( date_created__range=(start_date, end_date) ).annotate( month=TruncMonth('date_created') ).values('month').annotate( total=Count('id') ).order_by('month') # 4. 动态添加每个状态的计数注解 for status in all_statuses: # 将状态名转换为合法字段名(替换空格等特殊字符) field_name = status.replace(' ', '_') queryset = queryset.annotate( **{field_name: Count('id', filter=Q(status=status))} ) # 转换为列表格式的结果 result = list(queryset)
关键说明
- 动态获取
all_statuses时限定时间范围,避免统计无关的历史状态; - 状态名字段化处理:将状态名中的空格替换为下划线,确保符合Python标识符规则;
- 最终结果每个月份仅对应一个对象,包含该月总项目数及各状态的具体数量;
- 新增状态时无需修改代码,会自动纳入统计范围。
修正后示例输出
[{'month': datetime.datetime(2022, 5, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 2, 'Lost': 1, 'Open': 1, 'Won':0, 'Pending':0, 'Cancelled':0}, {'month': datetime.datetime(2022, 6, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'total': 1, 'Open': 1, 'Lost':0, 'Won':0, 'Pending':0, 'Cancelled':0}]
内容的提问来源于stack exchange,提问作者ChicagoMG2022
相关产品推荐
相关产品推荐

