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

如何合并两个Django查询集并保留正确统计结果?

解决Django ORM合并查询集时重复统计的问题

你遇到的重复计算问题,核心原因是多表关联产生的笛卡尔积:当你同时关联project__worklog和opportunity__turnover时,一个客户的每条工时记录会和每条营业额记录交叉匹配,导致Sum函数把相同的数值重复累加,最终结果被放大。

下面是纯Django ORM的解决方案,既避免重复统计,又能补全无数据的月份:

第一步:用子查询单独统计数值

我们先分别定义子查询,单独计算每个客户每个月的营业额和工时,这样可以避开笛卡尔积的问题:

from django.db.models import Subquery, OuterRef, Sum, Q, Coalesce
from django.db.models.functions import TruncMonth
from django.db.models import FloatField

# 子查询1:统计每个客户每个月的Project类型营业额
turnover_subquery = Turnover.objects.filter(
    opportunity__customer=OuterRef('pk'),
    type='Project',
    date__range=[start, end]
).annotate(
    month=TruncMonth('date')
).values('month').annotate(
    total_turnover=Sum('value')
).values('total_turnover')

# 子查询2:统计每个客户每个月的总工时和可计费工时
hours_subquery = Worklog.objects.filter(
    project__customer=OuterRef('pk'),
    day__range=[start, end]
).annotate(
    month=TruncMonth('day')
).values('month').annotate(
    total_hours=Sum('effort')/60/60,
    total_billed_hours=Sum(
        'effort',
        filter=Q(account__category__in=['Abrechenbar', 'Billable'])
    )/60/60
).values('total_hours', 'total_billed_hours')

第二步:生成目标月份列表(补全无数据月份)

为了让每个客户在统计周期内的每个月都有记录,我们先生成所有需要覆盖的月份:

from datetime import datetime

def get_target_months(start_date, end_date):
    """生成start到end之间的所有月初日期"""
    months = []
    current = start_date.replace(day=1)
    while current <= end_date:
        months.append(current)
        # 切换到下一个月
        if current.month == 12:
            current = current.replace(year=current.year + 1, month=1)
        else:
            current = current.replace(month=current.month + 1)
    return months

# 替换成你的实际起止日期
target_months = get_target_months(start, end)

第三步:交叉连接客户与月份,关联子查询

通过交叉连接客户和目标月份,再左关联子查询,最后用Coalesce把空值替换为0:

from django.db.models.expressions import RawSQL
from django.db.models import DateField

# 创建客户与所有目标月份的交叉查询(以PostgreSQL为例,其他数据库需调整)
customer_month_cross = Customer.objects.annotate(
    month=RawSQL(
        "SELECT unnest(%s::date[])",
        [target_months],
        output_field=DateField()
    )
)

# 最终查询:关联子查询并补全0值
final_queryset = customer_month_cross.annotate(
    # 关联营业额子查询,空值设为0
    turnover=Coalesce(
        Subquery(turnover_subquery.filter(month=OuterRef('month')), output_field=FloatField()),
        0.0
    ),
    # 关联工时子查询,空值设为0
    hours=Coalesce(
        Subquery(hours_subquery.filter(month=OuterRef('month')).values('total_hours'), output_field=FloatField()),
        0.0
    ),
    hoursBilled=Coalesce(
        Subquery(hours_subquery.filter(month=OuterRef('month')).values('total_billed_hours'), output_field=FloatField()),
        0.0
    )
).order_by('name', 'month').values('name', 'month', 'turnover', 'hours', 'hoursBilled')

关键细节说明

  1. 子查询的作用:每个子查询单独基于单条关联路径统计,完全避免了多表交叉导致的笛卡尔积,确保Sum只计算真实的记录数。
  2. 交叉连接月份:解决了无数据月份的补全需求,让每个客户在统计周期的每个月都有一条记录。
  3. Coalesce函数:将子查询返回的NULL值替换为0.0,让结果更符合统计需求。
  4. 数据库兼容性:上面的RawSQL用了PostgreSQL的unnest函数,如果是MySQL,需要用递归CTE来生成月份列表并做交叉连接。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:09:33