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

Django annotate统计主模型字段SUM值异常问题求助

问题根源

当你同时关联Tonic和Cross两个外键模型做聚合统计时,Django ORM会生成带笛卡尔积的SQL查询——主模型的单条记录会被重复关联多次(次数等于对应Tonic记录数 × Cross记录数),导致主模型字段的SUM被重复计算,最终数值被放大几十甚至上百倍。

解决方案:用子查询分离主模型与关联模型的统计

通过Subquery和OuterRef分别对主模型字段、关联模型的revenue做独立统计,彻底避免笛卡尔积影响,同时不会丢失数据。

具体实现步骤

假设你的模型结构大致如下:

class Statistic(models.Model):
    campaign_name = models.CharField(max_length=255)
    spend = models.DecimalField(max_digits=10, decimal_places=2)
    impressions = models.IntegerField()

class Tonic(models.Model):
    statistic = models.ForeignKey(Statistic, on_delete=models.CASCADE, related_name='tonics')
    revenue = models.DecimalField(max_digits=10, decimal_places=2)

class Cross(models.Model):
    statistic = models.ForeignKey(Statistic, on_delete=models.CASCADE, related_name='crosses')
    revenue = models.DecimalField(max_digits=10, decimal_places=2)

在你的StatisticViewSet中,修改get_queryset方法如下:

from django.db.models import Sum, Subquery, OuterRef

class StatisticViewSet(viewsets.ModelViewSet):
    serializer_class = StatisticSerializer

    def get_queryset(self):
        # 子查询统计主模型每个campaign_name的spend、impressions总和
        main_stats_subquery = Statistic.objects.filter(
            campaign_name=OuterRef('campaign_name')
        ).values('campaign_name').annotate(
            total_spend=Sum('spend'),
            total_impressions=Sum('impressions')
        ).values('total_spend', 'total_impressions')

        # 子查询统计每个campaign_name对应的Tonic revenue总和
        tonic_revenue_subquery = Tonic.objects.filter(
            statistic__campaign_name=OuterRef('campaign_name')
        ).values('statistic__campaign_name').annotate(
            total_tonic_revenue=Sum('revenue')
        ).values('total_tonic_revenue')

        # 子查询统计每个campaign_name对应的Cross revenue总和
        cross_revenue_subquery = Cross.objects.filter(
            statistic__campaign_name=OuterRef('campaign_name')
        ).values('statistic__campaign_name').annotate(
            total_cross_revenue=Sum('revenue')
        ).values('total_cross_revenue')

        # 主查询分组并关联子查询结果
        return Statistic.objects.values('campaign_name').annotate(
            total_spend=Subquery(main_stats_subquery.values('total_spend')[:1]),
            total_impressions=Subquery(main_stats_subquery.values('total_impressions')[:1]),
            total_tonic_revenue=Subquery(tonic_revenue_subquery.values('total_tonic_revenue')[:1], output_field=models.DecimalField()),
            total_cross_revenue=Subquery(cross_revenue_subquery.values('total_cross_revenue')[:1], output_field=models.DecimalField())
        ).order_by('campaign_name')

关键说明

  • 主模型的统计通过单独子查询完成,完全不关联Tonic和Cross,避免了笛卡尔积导致的重复计算。
  • 关联模型的revenue分别用独立子查询统计,互相不干扰,保证数据准确。
  • 每个Subquery末尾加[:1]确保只返回单个聚合值,符合子查询的返回要求。

内容的提问来源于stack exchange,提问作者Andrii

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 18:22:39