如何为Django QuerySet添加子查询统计的职位数量注解?
问题背景
构建求职板时,Location模型可表示国家、州、城市三级层级,通过slug的后缀关联父级区域(如州的slug为new-york-united-states,包含国家后缀)。需要为每个Location注解number_of_jobs,统计该区域及下属所有区域的关联职位总数(如纽约州需统计其下所有城市的职位)。
正确解决方案
使用Subquery结合虚拟分组的方式,避免Django生成不必要的GROUP BY,从而统计出正确的总数:
from django.db.models import Subquery, OuterRef, Count, Value, CharField # 构建子查询:通过虚拟字段分组,统计符合条件的distinct Job总数 job_count_subquery = Job.objects.filter( locations__slug__endswith=OuterRef("slug") ).distinct().annotate( # 添加虚拟字段,用于将所有结果归为一组 dummy=Value("", output_field=CharField()) ).values("dummy").annotate( total_jobs=Count("id") ).values("total_jobs")[:1] # 为每个Location注解职位总数 location_with_job_counts = Location.objects.annotate( number_of_jobs=Subquery(job_count_subquery) )
此方案会生成正确的统计结果:
- 曼哈顿:1个职位
- 布鲁克林:1个职位
- 纽约州:2个职位
- 美国:2个职位
各尝试失败原因解析
尝试1:直接使用Count("jobs")
Location.objects.annotate(number_of_jobs=Count("jobs", distinct=True))
失败原因:Count("jobs")仅统计直接关联到当前Location的Job数量。州/国家层级的Location没有直接关联的Job(Job都关联到城市层级Location),因此统计结果为0。
尝试2:使用count()作为Subquery
subquery = Job.objects.filter(locations__slug__endswith=OuterRef("slug")).distinct().count() Location.objects.annotate(number_of_jobs=Subquery(subquery))
失败原因:count()是立即执行的查询方法,会直接返回数据库查询的数值,而非查询集。Subquery要求传入的是未执行的查询集,因此此写法会报错。
尝试3:annotate导致不必要的GROUP BY
jobs = Job.objects.filter(locations__slug__endswith=OuterRef("slug")) subquery = jobs.annotate(job_count=Count("id", distinct=True)).values("job_count")[:1] Location.objects.annotate(number_of_jobs=Subquery(subquery))
失败原因:Django中,对模型QuerySet调用annotate()时,默认会按模型的主键字段(此处为Job.id)进行分组,目的是为每个Job对象添加注解字段。这会导致SQL中生成GROUP BY job.id,子查询返回的是单个Job的count值(恒为1),而非符合条件的Job总数。
Django自动添加GROUP BY的原因
当你对模型QuerySet使用annotate()时,Django的ORM逻辑是:为QuerySet中的每个对象生成注解字段。为了实现这一点,ORM会自动按模型的主键进行分组,确保每个对象对应一条注解结果。如果你的需求是对整个QuerySet进行聚合统计(而非每个对象),就需要通过虚拟分组(如上述方案中的dummy字段)来覆盖默认的分组行为。
内容的提问来源于stack exchange,提问作者Sam Hosseini

