Skip to main content

聚合

Django数据库抽象API 上的主题指南描述了您可以使用创建,检索,更新和删除单个对象的Django查询的方式。但是,有时您将需要检索通过汇总或 aggregating 对象集合派生的值。本主题指南介绍了使用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)
    num_awards = models.IntegerField()

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)
    pubdate = models.DateField()

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

作弊表

匆忙?以下是如何执行常见的聚合查询,假设上面的模型:

# 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.
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}

# Max price across all books.
>>> from django.db.models import Max
>>> Book.objects.all().aggregate(Max('price'))
{'price__max': Decimal('81.20')}

# Cost per page
>>> from django.db.models import F, FloatField, Sum
>>> Book.objects.all().aggregate(
...    price_per_page=Sum(F('price')/F('pages'), output_field=FloatField()))
{'price_per_page': 0.4470664529184653}

# 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

# 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 的对象的汇总值的方法。这是通过在 QuerySet 上附加一个 aggregate() 子句来完成的:

>>> 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 的终止子句,当被调用时,它返回一个名称 - 值对的字典。名称是聚合值的标识符;该值是计算的聚合。名称将从字段的名称和聚合函数自动生成。如果要手动指定聚合值的名称,可以在指定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() 组合将使用 产生错误的结果,因为使用联接而不是子查询:

>>> 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 Count, Min, Sum, Avg
>>> 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'))

(每个 Author 在结果 QuerySet 将有一个额外的属性称为 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 Count, Avg
>>> 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)

此查询生成带注释的结果集,然后基于该注释生成过滤器。

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如何将复杂的查询集转换为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'))

在本示例中,作者将按名称分组,因此您只会获取每个 unique 作者名称的注释结果。这意味着如果您有两个名字相同的作者,它们的结果将被合并到查询输出中的单个结果中;平均值将被计算为两位作者所写的书的平均值。

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() 子句之前,则任何注释都将自动添加到结果集中。但是,如果在 annotate() 子句后应用 values() 子句,则需要显式包含聚合列。

与默认排序或 order_by() 的交互

在选择输出数据时,在查询集的 order_by() 部分中提到的字段(或在模型上的默认排序中使用的字段),即使它们未在 values() 调用中另外指定。这些额外字段用于将“喜欢”结果分组在一起,并且它们可以使否则相同的结果行显得是分开的。这显示,特别是,当计数的事情。

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

from django.db import models

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

    class Meta:
        ordering = ["name"]

这里的重要部分是 name 字段上的默认排序。如果要计算每个不同 data 值的显示次数,您可以尝试此操作:

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

...将按照其公共 data 值将 Item 对象分组,然后计算每个组中的 id 值的数量。除了它不会工作。 name 的默认排序也将在分组中起作用,因此此查询将按不同的 (data, name) 对分组,这不是您想要的。相反,你应该构造这个查询集:

Item.objects.values("data").annotate(Count("id")).order_by()

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

此行为与在 distinct() 的查询集文档中所述的行为相同,通用规则是相同的:通常您不希望额外的列在结果中播放一部分,因此请清除排序,或至少确保其限制仅限于您在 values() 呼叫中选择的那些字段。

注解

你可能会合理地问为什么Django不会为你删除无关的列。主要原因是与 distinct() 和其他地方的一致性:Django 决不 删除您指定的顺序约束(并且我们不能更改其他方法的行为,因为这将违反我们的 API稳定性 策略)。

聚合注释

您还可以对注释的结果生成聚合。定义 aggregate() 子句时,您提供的聚合可以引用在查询中定义为 annotate() 子句一部分的任何别名。

例如,如果您想计算每本书的作者平均数,那么您首先使用作者数对该组图书进行注释,然后聚合该作者计数,引用注释字段:

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