Django ORM中F表达式结合Sum()聚合查询计算结果不符合预期
问题原因
你的ORM查询计算错误核心是跨多层一对多关联直接聚合触发了JOIN笛卡尔积问题:
当你直接在Order查询集上同时关联items、items__sales_tax、items__discount三个关联表做聚合时,数据库生成的左连接会对多表匹配结果做笛卡尔乘积,原本的税率、折扣、商品金额行被重复复制,此时你调用Sum(F('items__sales_tax__percentage'))统计的是重复行的总和,根本不是你单表直查得到的25,结果自然错误。
除此之外你的聚合层级也有逻辑问题:折扣、销售税都是绑定在单个订单项上的,你直接在订单维度统一累加所有商品金额、所有税率、所有折扣再计算,相当于把不同订单项的折扣、税率混算,哪怕没有JOIN重复问题,遇到不同订单项税率/折扣规则不同的场景,结果同样会出错。
另外你贴的模型代码有两处笔误:
models.ForiegnKey拼写错误,正确为models.ForeignKeyOrderItemDiscount关联的模型是OrderItem,但你定义的订单项模型类名为OrderItems(带复数s),运行时会报模型找不到错误,需要统一类名。
修复方案
要彻底避免JOIN重复问题,必须从最细粒度的订单项维度开始计算:先算出单条订单项的折后金额、对应税额,再向上聚合到订单维度,不要跨层级直接在订单表上聚合所有关联字段。
完整实现代码如下:
from django.db.models import Sum, F, ExpressionWrapper, DecimalField, OuterRef, Subquery, Case, When # 第一步:在订单项维度完成单条商品的金额、折扣、税额计算 order_item_agg = OrderItems.objects.filter(order=OuterRef('pk')).annotate( # 单条订单项原始货值 item_subtotal = F('trade_price') * F('quantity'), # 单条订单项折后金额,区分百分比折扣/固定金额折扣 item_discounted = Case( When( discount__in_percentage=True, then=ExpressionWrapper( F('item_subtotal') * (100 - F('discount__discount')) / 100, output_field=DecimalField() ) ), default=ExpressionWrapper( F('item_subtotal') - F('discount__discount'), output_field=DecimalField() ), output_field=DecimalField() ), # 单条订单项总税率 item_tax_rate = Sum(F('sales_tax__percentage')) / 100, # 单条订单项税后金额 item_after_tax = ExpressionWrapper( F('item_discounted') * (1 + F('item_tax_rate')), output_field=DecimalField() ) ).values('order_id').annotate( # 聚合单订单下所有订单项的金额 total_trade = Sum('item_subtotal'), total_discounted = Sum('item_discounted'), total_tax = Sum('item_after_tax') - Sum('item_discounted'), total_final = Sum('item_after_tax') ) # 第二步:通过子查询将订单项聚合结果关联到订单查询集 orders = Order.objects.filter( customer__zone__distribution=get_user_distribution(request.user.id) ).annotate( trade_price = Subquery(order_item_agg.values('total_trade')), tax = Subquery(order_item_agg.values('total_tax')), discounted_price = Subquery(order_item_agg.values('total_discounted')), total = Subquery(order_item_agg.values('total_final')) )
注意事项
- 你原来的代码完全没有处理
in_percentage字段的分支逻辑,所有折扣都按百分比计算,遇到固定金额折扣的场景计算结果会完全错误,上面的代码已经补全了这部分判断。 - 只要在查询中同时join两个及以上一对多关联表,就不要直接在根查询集上调用
Sum做聚合,必然会出现笛卡尔积导致的重复计算问题,必须按照「最细粒度子表先聚合,再向上关联父表」的思路写查询。
内容的提问来源于stack exchange,提问作者Muhammad Saqib
相关产品推荐
相关产品推荐

