聚合¶
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查询!
为了理解查询中发生的情况,请考虑检查 QuerySet
的 query
属性。
连接和聚合¶
到目前为止,我们已经处理了属于被查询模型的字段的聚合。但是,有时您要聚合的值将属于与您要查询的模型相关的模型。
当指定要在聚合函数中聚合的字段时,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}