如何在Django中不使用values()实现GROUP BY并返回模型实例QuerySet
首先明确SQL基础逻辑:标准GROUP BY语法会将指定字段值相同的多行合并为单条聚合结果,单条聚合结果无法直接映射分组内包含的所有原始模型记录,以下方案根据不同业务场景返回带聚合注解的模型实例QuerySet,替代values()返回的字典结果集。
方案1:从关联主表侧发起查询(推荐,适用于分组字段为外键的场景)
如果你的category字段是关联Category模型的外键字段,直接从关联的主模型侧查询,天然返回模型实例,不需要调用values()。
假设模型定义如下:
class Category(models.Model): name = models.CharField(max_length=100) class Participant(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="participants") price = models.DecimalField(max_digits=10, decimal_places=2)
查询代码:
from django.db.models import Avg, F # 返回Category模型的常规QuerySet,每个实例自带分组聚合的average属性 category_qs = Category.objects.annotate( average=Avg(F("participants__price")) ) # 直接读取实例属性即可 for cat in category_qs: print(cat.name, cat.average)
这个方案是Django ORM最推荐的分组写法,完全符合ORM关联查询设计,无额外性能损耗。
方案2:使用窗口函数(适用于需要返回所有原始模型实例、同时携带分组聚合值的场景)
如果你需要返回所有Participant模型实例,且每个实例上附带其所属category分组的平均价格,可以用窗口函数替代GROUP BY,不需要合并行,返回结果全是Participant模型实例:
from django.db.models import Avg, F, Window participant_qs = Participant.objects.annotate( average=Window( expression=Avg(F("price")), partition_by=F("category") # 按category分区,对应分组维度 ) )
注意:窗口函数不会做行合并,同一个category下的所有Participant记录都会被返回,同组记录的
average属性值完全相同。
方案3:子查询+去重(适用于分组字段为普通字段、单分组返回单条模型实例的场景)
如果category是存字符串的普通字段(不是外键),且需要和原values()分组逻辑保持一致(每个category只返回一条结果),可以通过子查询给实例注解聚合值后按分组字段去重:
from django.db.models import Avg, F, OuterRef, Subquery # 构建分组聚合的子查询 avg_sub = Participant.objects.filter( category=OuterRef("category") ).annotate(avg=Avg("price")).values("avg")[:1] # 给原始实例注解聚合值,按category去重,每个分组取第一条实例 participant_qs = Participant.objects.annotate( average=Subquery(avg_sub) ).distinct("category")
注意:带字段参数的
distinct()方法仅支持PostgreSQL数据库,如果使用MySQL等其他数据库,可以在业务层做二次遍历去重;如果需要指定每个分组返回的实例规则,可以在distinct()前加order_by('category', '-price')这类排序规则,自定义分组内返回的实例。
内容的提问来源于stack exchange,提问作者znat

