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
相关产品推荐
相关产品推荐

