Django中添加外键关联的annotate后查询结果不唯一及统计值异常问题
这个坑我之前也踩过!问题出在UserOrder和UserOrderService的一对多关联上:当你直接在查询里加入Count("userorderservice__pk")时,Django会自动把两张表做JOIN,每个UserOrder会和它关联的所有UserOrderService记录拼接在一起。结果就是——原本的订单数统计会把同一个订单重复算N次(N是该订单的服务数量),总金额也会被重复累加N次,自然数值就不对了。
下面给你两种靠谱的解决办法,按需选择:
方法一:用子查询单独统计服务数量(推荐,适合大数据量)
这种方式不会干扰主查询的聚合逻辑,用Django的Subquery和OuterRef来单独拉取每个用户的服务总数:
先导入需要的模块:
from django.db.models import Subquery, OuterRef, Count, Sum from django.db.models.functions import Coalesce
然后写查询语句:
# 先构造子查询:统计单个用户所有订单关联的服务总数 service_count_subquery = UserOrderService.objects.filter( order_id__user_id=OuterRef('user_id') ).values('order_id__user_id').annotate( service_count=Count('pk') ).values('service_count') # 主查询:先搞定用户的订单数、总金额,再通过子查询注入服务数 final_query = UserOrder.objects.filter( order_date__range=(Request['from'], Request['to']) ).values('user_id__username', 'user_id__email', 'user_id').annotate( total_no_of_order=Count('pk'), total=Sum('total_cost') ).annotate( total_no_of_services=Coalesce(Subquery(service_count_subquery), 0) )
关键点解释:
OuterRef('user_id')用来关联主查询里的用户ID,让子查询能针对每个用户单独统计。Coalesce是为了兜底:如果某个用户没有任何关联服务,会返回0而不是NULL,避免后续处理出问题。- 主查询先基于UserOrder完成分组统计,再通过子查询把服务数加进来,完全不会因为JOIN导致重复统计。
方法二:先统计主数据,再补服务数(简单直观,适合小数据量)
如果你的数据量不大,这种写法更易懂:先统计好用户的订单数和总金额,再循环给每个结果补服务数量:
from django.db.models import Count, Sum # 第一步:获取用户的订单统计数据 user_order_stats = UserOrder.objects.filter( order_date__range=(Request['from'], Request['to']) ).values('user_id__username', 'user_id__email', 'user_id').annotate( total_no_of_order=Count('pk'), total=Sum('total_cost') ) # 第二步:给每个统计结果添加服务总数 for stat in user_order_stats: stat['total_no_of_services'] = UserOrderService.objects.filter( order_id__user_id=stat['user_id'] ).count()
这种方法不用写复杂的子查询,新手也能快速理解,唯一要注意的是如果数据量很大,循环查询可能会有性能问题,这时候优先选方法一。
再复盘下问题根源
你之前的写法里,一旦加入Count("userorderservice__pk"),Django会执行INNER JOIN把两张表拼在一起。举个例子:假设用户A有1个订单,这个订单关联了3个服务,那么JOIN后的结果集中,这个订单会出现3次。这时候Count('pk')会把这3次都算进去,所以订单数变成了3而不是1;Sum('total_cost')也会把这个订单的金额加3次,总金额自然就错了。
用子查询的方式相当于分开统计两个维度的数据,再把结果合并,完美避开了JOIN带来的重复问题。
内容的提问来源于stack exchange,提问作者Ajay Chaurasia

