Django4 中文入门教程 Django4.0 聚合-聚合和其他QuerySet子句

2024-02-25 开发教程 Django4 中文入门教程 匿名 5

filter() 和 exclude()

聚合也可以参与过滤。任何应用于普通模型字段的 ​filter()​ (或 ​exclude()​)会具有约束被认为是聚合的对象的效果。
当使用 ​annotate()​ 子句,过滤器具有约束计算注解的对象的效果。比如,你可以使用查询生成一个所有书籍的注解列表,这个列表的标题以 "Django" 开头。

>>> from django.db.models import Avg, Count
>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))

当使用 ​aggregate()​ 子句,过滤器将具有约束计算聚合的对象的效果。比如,你可以使用查询生成所有标题以 "Django" 开头的平均价格。

>>> Book.objects.filter(name__startswith="Django").aggregate(Avg('price'))

过滤注解

注解过的值也可以使用过滤器。注解的别名可以和任何其他模型字段一样使用 ​filter()​ 和 ​exclude()​ 子句。

比如,要生成多名作者的书籍列表,可以发出这种查询:

>>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)

这个查询生成一个注解结果集,然后生成一个基于注解的过滤器。
如果你需要两个带有两个独立的过滤器的注解,你可以在任何聚合中使用 ​filter ​语句。比如,要生成一个带有高评价书籍的作者列表:

>>> highly_rated = Count('book', filter=Q(book__rating__gte=7))
>>> Author.objects.annotate(num_books=Count('book'), highly_rated_books=highly_rated)

结果集中的每个 Author 都有 ​num_books ​和 ​highly_rated_books ​属性。

annotate() 和 filter() 子句的顺序

当开发一个涉及 ​annotate()​ 和 ​filter()​ 子句的复杂查询时,要特别注意应用于 ​QuerySet ​的子句的顺序。
当一个 ​annotate()​ 子句应用于查询,会根据查询状态来计算注解,直到请求的注解为止。这实际上意味着 ​filter()​ 和 ​annotate()​ 不是可交换的操作。
比如:

  • 出版者A有两本评分4和5的书。
  • 出版者B有两本评分1和4的书。
  • 出版者C有一本评分1的书。

下面就是 ​Count ​聚合的例子:

>>> a, b = Publisher.objects.annotate(num_books=Count('book', distinct=True)).filter(book__rating__gt=3.0)
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 2)
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book'))
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 1)

两个查询返回出版者列表,这些出版者至少有一本评分3的书,因此排除了C。
在第一个查询里,注解优先于过滤器,因此过滤器没有影响注解。​distinct=True​ 用来避免一个 ​query ​bug。
第二个查询每个发布者评分3以上的书籍数量。过滤器优先于注解,因此过滤器约束计算注解时考虑的对象。
这里是另一个关于 Avg 聚合的例子:

>>> a, b = Publisher.objects.annotate(avg_rating=Avg('book__rating')).filter(book__rating__gt=3.0)
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 2.5) # (1+4)/2
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(avg_rating=Avg('book__rating'))
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 4.0) # 4/1 (book with rating 1 excluded)

第一个查询请求至少有一本评分3以上的书籍的出版者的书籍平均分。第二个查询只请求评分3以上的作者书籍的平均评分。
很难凭直觉了解ORM如何将复杂的查询集转化为SQL查询,因此当有疑问时,请使用 ​str(queryset.query)​ 检查SQL,并写大量的测试。

order_by()

注解可以当做基本排序来使用。当你定义了一个 ​order_by()​ 子句,你提供的聚合可以引用任何定义为查询中 ​annotate()​ 子句的一部分的别名。
比如,通过书籍的作者数量来对书籍的 ​QuerySet ​排序,你可以使用下面的查询:

>>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')

values()

通常,注解值会添加到每个对象上,即一个被注解的 ​QuerySet ​将会为初始 ​QuerySet ​的每个对象返回一个结果集。然而,当使用 ​values()​ 子句来对结果集进行约束时,生成注解值的方法会稍有不同。不是在原始 ​QuerySet ​中对每个对象添加注解并返回,而是根据定义在 ​values()​ 子句中的字段组合先对结果进行分组,再对每个单独的分组进行注解,这个注解值是根据分组中所有的对象计算得到的。
下面是一个关于作者的查询例子,查询每个作者所著书的平均评分:

>>> Author.objects.annotate(average_rating=Avg('book__rating'))

这段代码返回的是数据库中的所有作者及其所著书的平均评分。
但是如果你使用 ​values()​ 子句,结果会稍有不同:

>>> Author.objects.values('name').annotate(average_rating=Avg('book__rating'))

在这个例子中,作者会按名字分组,所以你只能得到不重名的作者分组的注解值。这意味着如果你有两个作者同名,那么他们原本各自的查询结果将被合并到同一个结果中;两个作者的所有评分都将被计算为一个平均分。

annotate() 和 values() 的顺序

和使用 ​filter()​ 一样,作用于某个查询的 ​annotate()​ 和 ​values()​ 子句的顺序非常重要。如果 ​values()​ 子句在 ​annotate()​ 之前,就会根据 ​values()​ 子句产生的分组来计算注解。
然而如果 ​annotate()​ 子句在 ​values()​ 之前,就会根据整个查询集生成注解。这种情况下,​values()​ 子句只能限制输出的字段。
举个例子,如果我们颠倒上个例子中 ​values()​ 和 ​annotate()​ 的顺序:

>>> Author.objects.annotate(average_rating=Avg('book__rating')).values('name', 'average_rating')

这段代码将为每个作者添加一个唯一注解,但只有作者姓名和 ​average_rating ​注解会返回在输出结果中。
你应该也会注意 ​average_rating ​已经明确包含在返回的值列表中。这是必需的,因为 ​values()​ 和 ​annotate()​ 子句的顺序。
如果 ​values()​ 子句在 ​annotate()​ 子句之前,任何注解将自动添加在结果集中。然而,如果 ​values()​ 子句应用在 ​annotate()​ 子句之后,则需要显式包含聚合列。

与 order_by() 交互

在选择输出数据时使用查询集的 ​order_by()​ 部分中提到的字段,即使在 ​values()​ 调用中没有另外指定它们也是如此。 这些额外的字段用于将“​like​”的结果组合在一起,它们可以使原本相同的结果行看起来是分开的。 尤其是在计算事物时,这一点会出现。

举个例子,假设你有这样的模型:

from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
data = models.IntegerField()

如果您想计算每个不同数据值在有序查询集中出现的次数,您可以试试这个:

items = Item.objects.order_by('name')
# Warning: not quite correct!
items.values('data').annotate(Count('id'))

它将按 ​Item ​对象的公共数据值对它们进行分组,然后计算每组中 ​id ​值的数量。 除非它不会完全工作。 按名称排序也将在分组中发挥作用,因此此查询将按不同的(数据,名称)对分组,这不是您想要的。 相反,您应该构造这个查询集:

items.values('data').annotate(Count('id')).order_by()

清除任何查询中的排序。你也可以通过 ​data ​排序,没有任何有害影响,因为它已经在查询中发挥了作用。

这个行为与 ​distinct()​ 的行为相同,一般规则是一样的:通常情况下,你不希望额外的列在结果中发挥作用,因此要清除排序,或者至少确保它只限于您在 ​values()​ 调用中选择的那些字段。

聚合注解

你也可以在注解结果上生成聚合。当你定义 ​aggregate()​ 子句时,你提供的聚合可以引用任何定义在查询中 ​annotate()​ 子句的别名。
比如,如果你想计算每本书的平均作者数,首先使用作者数注解书籍集合,然后引用注解字段聚合作者数:

>>> from django.db.models import Avg, Count
>>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))
{'num_authors__avg': 1.66}