聚合

关于Django 的数据库抽象 API的主题指南描述了如何使用 Django 查询来创建、检索、更新和删除单个对象。但是,有时您需要检索通过汇总或聚合对象集合而得出的值。本主题指南介绍了使用 Django 查询生成和返回聚合值的方法。

在本指南中,我们将参考以下模型。这些模型用于跟踪一系列在线书店的库存

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()


class Publisher(models.Model):
    name = models.CharField(max_length=300)


class Book(models.Model):
    name = models.CharField(max_length=300)
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    rating = models.FloatField()
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    pubdate = models.DateField()


class Store(models.Model):
    name = models.CharField(max_length=300)
    books = models.ManyToManyField(Book)

备忘单

时间紧迫?以下是假设上述模型的情况下如何执行常见的聚合查询

# Total number of books.
>>> Book.objects.count()
2452

# Total number of books with publisher=BaloneyPress
>>> Book.objects.filter(publisher__name="BaloneyPress").count()
73

# Average price across all books, provide default to be returned instead
# of None if no books exist.
>>> from django.db.models import Avg
>>> Book.objects.aggregate(Avg("price", default=0))
{'price__avg': 34.35}

# Max price across all books, provide default to be returned instead of
# None if no books exist.
>>> from django.db.models import Max
>>> Book.objects.aggregate(Max("price", default=0))
{'price__max': Decimal('81.20')}

# Difference between the highest priced book and the average price of all books.
>>> from django.db.models import FloatField
>>> Book.objects.aggregate(
...     price_diff=Max("price", output_field=FloatField()) - Avg("price")
... )
{'price_diff': 46.85}

# All the following queries involve traversing the Book<->Publisher
# foreign key relationship backwards.

# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count("book"))
>>> pubs
<QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
>>> pubs[0].num_books
73

# Each publisher, with a separate count of books with a rating above and below 5
>>> from django.db.models import Q
>>> above_5 = Count("book", filter=Q(book__rating__gt=5))
>>> below_5 = Count("book", filter=Q(book__rating__lte=5))
>>> pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5)
>>> pubs[0].above_5
23
>>> pubs[0].below_5
12

# The top 5 publishers, in order by number of books.
>>> pubs = Publisher.objects.annotate(num_books=Count("book")).order_by("-num_books")[:5]
>>> pubs[0].num_books
1323

QuerySet上生成聚合

Django 提供两种生成聚合的方法。第一种方法是在整个QuerySet上生成汇总值。例如,假设您想计算所有待售书籍的平均价格。Django 的查询语法提供了一种描述所有书籍集合的方法

>>> Book.objects.all()

我们需要一种方法来计算属于此QuerySet的对象的汇总值。这是通过将aggregate()子句附加到QuerySet来完成的

>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg("price"))
{'price__avg': 34.35}

在本例中,all()是冗余的,因此可以简化为

>>> Book.objects.aggregate(Avg("price"))
{'price__avg': 34.35}

aggregate()子句的参数描述了我们想要计算的聚合值——在本例中,是Book模型上price字段的平均值。可在QuerySet 参考中找到可用的聚合函数列表。

aggregate()QuerySet的终端子句,调用时返回名称值对字典。名称是聚合值的标识符;值是计算出的聚合值。名称会根据字段名称和聚合函数自动生成。如果您想手动指定聚合值的名称,则可以在指定聚合子句时提供该名称

>>> Book.objects.aggregate(average_price=Avg("price"))
{'average_price': 34.35}

如果您想生成多个聚合,请向aggregate()子句添加另一个参数。因此,如果我们还想了解所有书籍的最高价和最低价,我们将发出以下查询

>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg("price"), Max("price"), Min("price"))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}

QuerySet中的每个项目生成聚合

生成汇总值的第二种方法是为QuerySet中的每个对象生成一个独立的汇总。例如,如果您正在检索书籍列表,您可能想知道有多少作者为每本书做出了贡献。每本书与作者之间存在多对多关系;我们想为QuerySet中的每本书汇总此关系。

可以使用annotate()子句生成每个对象的汇总。当指定annotate()子句时,QuerySet中的每个对象都将使用指定的值进行注释。

这些注释的语法与aggregate()子句使用的语法相同。annotate()的每个参数都描述要计算的聚合。例如,要使用作者数量来注释书籍

# Build an annotated queryset
>>> from django.db.models import Count
>>> q = Book.objects.annotate(Count("authors"))
# Interrogate the first object in the queryset
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# Interrogate the second object in the queryset
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1

aggregate()一样,注释的名称会根据聚合函数的名称和正在聚合的字段的名称自动派生。您可以在指定注释时提供别名来覆盖此默认名称

>>> q = Book.objects.annotate(num_authors=Count("authors"))
>>> q[0].num_authors
2
>>> q[1].num_authors
1

aggregate()不同,annotate()不是终端子句。annotate()子句的输出是QuerySet;可以使用任何其他QuerySet操作(包括filter()order_by(),甚至是对annotate()的额外调用)修改此QuerySet

组合多个聚合

使用annotate()组合多个聚合将产生错误的结果,因为使用了连接而不是子查询

>>> book = Book.objects.first()
>>> book.authors.count()
2
>>> book.store_set.count()
3
>>> q = Book.objects.annotate(Count("authors"), Count("store"))
>>> q[0].authors__count
6
>>> q[0].store__count
6

对于大多数聚合,无法避免此问题,但是,Count聚合具有一个distinct参数,这可能会有所帮助

>>> q = Book.objects.annotate(
...     Count("authors", distinct=True), Count("store", distinct=True)
... )
>>> q[0].authors__count
2
>>> q[0].store__count
3

如有疑问,请检查 SQL 查询!

为了了解您的查询中发生了什么,请考虑检查QuerySetquery属性。

连接和聚合

到目前为止,我们已经处理了属于正在查询的模型的字段的聚合。但是,有时您想要聚合的值将属于与您正在查询的模型相关的模型。

在聚合函数中指定要聚合的字段时,Django 将允许您使用与在过滤器中引用相关字段时使用的相同的双下划线表示法。然后,Django 将处理检索和聚合相关值所需的任何表连接。

例如,要查找每个商店提供的书籍的价格范围,您可以使用以下注释

>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min("books__price"), max_price=Max("books__price"))

这告诉 Django 检索Store模型,通过多对多关系与Book模型连接,并在书籍模型的价格字段上进行聚合以生成最小值和最大值。

相同的规则也适用于aggregate()子句。如果您想知道在任何商店中待售的任何书籍的最低价和最高价,您可以使用以下聚合

>>> Store.objects.aggregate(min_price=Min("books__price"), max_price=Max("books__price"))

连接链可以根据您的需要任意深入。例如,要提取任何待售书籍的最年轻作者的年龄,您可以发出以下查询

>>> Store.objects.aggregate(youngest_age=Min("books__authors__age"))

向后跟踪关系

跨越关系的查找类似,对模型或与您正在查询的模型相关的模型的字段的聚合和注释可以包括遍历“反向”关系。这里也使用相关模型的小写名称和双下划线。

例如,我们可以请求所有出版商,并使用它们各自的总图书库存计数进行注释(请注意,我们如何使用'book'来指定Publisher -> Book反向外键跳转)

>>> from django.db.models import Avg, Count, Min, Sum
>>> Publisher.objects.annotate(Count("book"))

(结果QuerySet中的每个Publisher都将具有一个名为book__count的额外属性。)

我们还可以请求每个出版商管理的所有书籍中最旧的书籍

>>> Publisher.objects.aggregate(oldest_pubdate=Min("book__pubdate"))

(结果字典将有一个名为'oldest_pubdate'的键。如果没有指定此类别名,它将是相当长的'book__pubdate__min'。)

这不仅适用于外键。它也适用于多对多关系。例如,我们可以请求每个作者,并使用作者(共同)创作的所有书籍的总页数进行注释(请注意,我们如何使用'book'来指定Author -> Book反向多对多跳转)

>>> Author.objects.annotate(total_pages=Sum("book__pages"))

(结果QuerySet中的每个Author都将具有一个名为total_pages的额外属性。如果没有指定此类别名,它将是相当长的book__pages__sum。)

或者查询我们数据库中所有作者撰写的书籍的平均评分

>>> Author.objects.aggregate(average_rating=Avg("book__rating"))

(结果字典将包含一个名为 'average_rating' 的键。如果没有指定此别名,则将使用较长的名称 'book__rating__avg'。)

聚合和其他 QuerySet 子句

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_bookshighly_rated_books 属性。另请参见 条件聚合

filterQuerySet.filter() 之间进行选择

避免对单个注释或聚合使用 filter 参数。使用 QuerySet.filter() 排除行效率更高。只有当对相同关系使用两个或多个具有不同条件的聚合时,聚合 filter 参数才有用。

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.0 的出版商列表,因此排除了出版商 C。

在第一个查询中,注释先于过滤器,因此过滤器对注释没有影响。distinct=True 是必需的,以避免 查询错误

第二个查询计算每个出版商评分超过 3.0 的书籍数量。过滤器先于注释,因此过滤器会限制计算注释时考虑的对象。

这是另一个使用 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.0 的出版商的所有书籍的平均评分。第二个查询询问仅对于超过 3.0 的评分的出版商书籍评分的平均值。

很难直观地理解 ORM 如何将复杂的 QuerySet 转换为 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() 的交互

在queryset 的 order_by() 部分中提到的字段用于选择输出数据,即使它们没有在 values() 调用中另行指定。这些额外的字段用于将“相似”的结果组合在一起,它们可能会使原本相同的result rows 看起来是分开的。这尤其在计数时出现。

例如,假设您有一个这样的模型:

from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=10)
    data = models.IntegerField()

如果您想计算每个不同的 data 值在排序的 queryset 中出现的次数,您可以尝试:

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

……它将按它们的公共 data 值对 Item 对象进行分组,然后计算每个组中 id 值的数量。但它不会完全有效。按 name 排序也会在分组中发挥作用,因此此查询将按不同的 (data, name) 对进行分组,这不是您想要的。相反,您应该构建此queryset:

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

……清除查询中的任何排序。您也可以按例如 data 排序,而不会产生任何有害影响,因为这已经在查询中发挥作用。

此行为与distinct()在queryset文档中提到的行为相同,其一般规则也相同:通常情况下,您不希望额外的列参与结果,因此请清除排序,或者至少确保排序仅限于您在values()调用中选择的那些字段。

注意

您可能会合理地问为什么Django不为您删除多余的列。主要原因是与distinct()和其他地方保持一致:Django**从不**删除您指定的排序约束(我们无法更改其他方法的行为,因为这会违反我们的API稳定性策略)。

聚合注释

您还可以根据注释的结果生成聚合。当您定义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}

在空queryset或分组上进行聚合

当聚合应用于空queryset或分组时,结果默认为其默认参数,通常为None。出现此行为是因为当执行的查询不返回任何行时,聚合函数返回NULL

您可以通过为大多数聚合提供default参数来指定返回值。但是,由于Count不支持default参数,因此对于空queryset或分组,它将始终返回0

例如,假设没有任何书的名称包含web,则计算此书籍集的总价格将返回None,因为没有匹配的行可以对Sum聚合进行计算。

>>> from django.db.models import Sum
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price"))
{"price__sum": None}

但是,当调用Sum时,可以设置default参数,以便在找不到任何书籍时返回不同的默认值。

>>> Book.objects.filter(name__contains="web").aggregate(Sum("price", default=0))
{"price__sum": Decimal("0")}

在底层,default参数是通过使用Coalesce包装聚合函数来实现的。

返回顶部