You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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等取值,需要查询指定时间范围内每月各状态的项目数量。

尝试了以下代码,但存在两个核心问题:

  1. 同一月份会生成多个对象,每种有项目的状态对应一个独立对象,分组逻辑异常;
  2. 状态取值为硬编码,新增状态时必须手动修改代码。

尝试的代码:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 03:15:36