聚集

主题指南 Django's database-abstraction 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() 子句描述我们要计算的聚合值-在本例中,是 price 场上 Book 模型。可用的聚合函数列表可以在 QuerySet reference .

aggregate() 的终止子句。 QuerySet 它在被调用时返回名称-值对的字典。名称是聚合值的标识符;值是计算出的聚合值。该名称是根据字段名称和聚合函数自动生成的。如果要手动指定聚合值的名称,可以通过在指定Aggregate子句时提供该名称来实现:

>>> 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()not 终止子句。的输出 annotate() 子句是 QuerySet 这个 QuerySet 可以使用任何其他 QuerySet 操作,包括 filter()order_by() 或是调用给 annotate() .

组合多个聚合

将多个聚合与 annotate()yield the wrong results 因为使用联接而不是子查询:

>>> 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查询!

为了了解查询中发生的情况,请考虑检查 query 您的财产 QuerySet .

连接和聚合

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

当指定要在聚合函数中聚合的字段时,Django将允许您使用相同的字段 double underscore notation 在引用筛选器中的相关字段时使用。然后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"))

(每一个 Publisher 在结果中 QuerySet 将有一个名为 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"))

(每一个 Author 在结果中 QuerySet 将有一个名为 total_pages . 如果没有指定这样的别名,它将是相当长的 book__pages__sum

或者问一下我们存档的所有作者(S)的书的平均评分:

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

(生成的字典将具有一个名为 'average_rating' . 如果没有指定这样的别名,它将是相当长的 'book__rating__avg'

聚合和其他 QuerySet 条款

filter() and 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 必须避免 query bug .

第二个查询统计每个出版商的分级超过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将如何将复杂的查询集转换为SQL查询,因此如果有疑问,请使用 str(queryset.query) 写很多测试。

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()

如果你想计算每一个不同的 data 值出现在有序查询集中,您可以尝试执行以下操作::

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

...这将分组 Item 对象按它们的公共 data 值,然后计算 id 每组中的值。只是它不太管用。排序依据 name 还将在分组中发挥作用,因此此查询将按DISTINCT分组 (data, name) 配对,这不是你想要的。相反,您应该构造这个查询集::

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

…清除查询中的任何排序。你也可以点菜,比如, data 没有任何有害影响,因为它已经在查询中扮演了一个角色。

此行为与的查询集文档中指出的行为相同。 distinct() 一般的规则是一样的:通常情况下,您不会希望额外的列在结果中扮演一个角色,所以请清除排序,或者至少确保它仅限于您在 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}

在空查询集或组上聚合

将聚合应用于空查询集或分组时,结果缺省为其 default 参数,通常 None 。发生此行为是因为聚合函数返回 NULL 当执行的查询不返回任何行时。

方法,可以指定返回值 default 大多数聚合的参数。然而,由于 Count 不支持 default 参数,则它将始终返回 0 用于空查询集或组。

例如,假设没有一本书包含 web 以它的名义,计算这套书的总价将返回 None 由于没有匹配的行来计算 Sum 聚合于:

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

然而, default 参数可以在调用时设置 Sum 如果找不到任何帐簿,则返回不同的默认值:

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

在引擎盖下, default 参数是通过将聚合函数包装为 Coalesce