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

Django中aggregate与distinct联用报错的ORM优化方案咨询

问题描述

我有一个FilterSet,包含以下属性:

dosFromGte = filters.DateFilter(method="search_by_dos_from_gte", lookup_expr="gte")
dosToLte = filters.DateFilter(method="search_by_dos_from_lte", lookup_expr="lte")

# 其中一个方法实现:
def search_by_dos_from_lte(self, queryset: Chart, name: str, value: str) -> Chart:
    return queryset.annotate(max_dos=Min("diagnosis__dos_from")).filter(max_dos__lte=value)

这里使用了annotate和聚合函数Min()。

我需要先执行若干注解操作,然后仅获取唯一的ID。原代码如下:

queryset = self.filter_queryset(self.get_queryset().exclude(state__in=repo_exclude))
queryset = queryset.annotate(
            ChartId=F("chart_id")
        ).values(
            "ChartId"
        ).distinct("id")

当应用过滤器参数?clientId=11&project=56&dosFromGte=2023-08-17&dosToLte=2023-08-19时,会先执行Min()聚合再按id去重,此时抛出错误:

NotImplementedError("aggregate() + distinct(fields) not implemented.")

我尝试了临时解决方案,但不够优雅:

queryset.values(
            "id", "ChartId"
        )
# 根据主键id去除重复的chart记录
df = df.drop_duplicates('id')
df = df.drop('id', axis=1)

请问是否有更简洁的ORM原生实现方式?

解决方案

方法1:简化distinct用法

如果ChartId与id是一一对应的(主键关联),可以去掉distinct("id")的字段参数,直接使用无参的distinct(),让数据库基于返回的ChartId字段去重:

queryset = self.filter_queryset(self.get_queryset().exclude(state__in=repo_exclude))
queryset = queryset.annotate(
    ChartId=F("chart_id")
).values("ChartId").distinct()

注意:该方式依赖ChartId本身的唯一性,如果ChartId可能重复但你需要按id去重,此方法不适用。

方法2:拆分查询,用子查询分离聚合与去重

先通过子查询获取所有符合过滤条件的Chart主键,再基于这些主键查询并提取ChartId,绕开聚合与字段级distinct的冲突:

from django.db.models import Subquery

# 第一步:获取经过过滤(含聚合逻辑)的Chart主键集合
filtered_charts = self.filter_queryset(self.get_queryset().exclude(state__in=repo_exclude))
valid_ids = filtered_charts.values_list('id', flat=True)

# 第二步:基于主键提取唯一的ChartId
queryset = Chart.objects.filter(id__in=valid_ids).annotate(
    ChartId=F("chart_id")
).values("ChartId").distinct()

也可以写成更紧凑的子查询形式:

from django.db.models import Subquery, OuterRef

valid_charts = self.filter_queryset(self.get_queryset().exclude(state__in=repo_exclude))
queryset = Chart.objects.filter(
    id__in=Subquery(valid_charts.values('id'))
).annotate(ChartId=F("chart_id")).values("ChartId").distinct()

方法3:调整FilterSet的过滤逻辑,用子查询替代主查询聚合

修改FilterSet中的日期过滤方法,使用Exists子查询实现聚合判断,让主查询不携带聚合字段,这样就能正常使用distinct("id"):

from django.db.models import Exists, OuterRef, Min

def search_by_dos_from_lte(self, queryset: Chart, name: str, value: str) -> Chart:
    return queryset.filter(
        Exists(
            Diagnosis.objects.filter(
                chart_id=OuterRef('id')
            ).values('chart_id').annotate(min_dos=Min('dos_from')).filter(min_dos__lte=value)
        )
    )

之后原有的查询逻辑即可正常运行:

queryset = self.filter_queryset(self.get_queryset().exclude(state__in=repo_exclude))
queryset = queryset.annotate(
    ChartId=F("chart_id")
).values("ChartId").distinct("id")

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 12:34:54