参考文献:Django官方文档 模型聚合 友情赞助:有道词典
目录
Generating aggregates for each item in a QuerySet(为 QuerySet 中的每个项生成聚合)
combining multiple aggregations(结合多个聚合)
Following relationships backwards(反向关系)
Aggregations and other QuerySet clauses(聚合和其他 QuerySet 字句)
聚合(Aggregation)
Django 的数据库抽象 API 描述了使用 Django queries 来增删改查单个对象的方法。然而,有时候你要获取的值需要根据一组对象聚合后才能得到。这个主题指南描述了如何使用 Django queries 来生成和返回聚合值的方法。
整篇指南我们将引用以下模型。这些模型用来记录多个网上书店的库存:
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.
>>> 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')}
# 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 对象的汇总值。(PS: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 referance 中找到可用的聚合函数列表。
aggregate() 是 QuerySet 的一个终端子句,当调用它时,返回一个 name-value 字典。name 是聚合值的标识符;该值是计算得到的聚合。name 由字段名和聚合函数名自动生成。如果您想手动指定聚合值的名称,可以在指定聚合子句时提供该名称:
>>> 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')}
Generating aggregates for each item in a QuerySet(为 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() 一样,annotation 的名称自动派生自聚合函数的名称和被聚合字段的名称。您可以通过在指定注释时提供别名来覆盖此默认名称:
>>> q = Book.objects.annotate(num_authors=Count('authors'))
>>> q[0].num_authors
2
>>> q[1].num_authors
1
与 aggregate() 不同,annotate() 不是一个终端子句。annotate() 子句的输出是 QuerySet;可以使用任何其他 QuerySet 操作修改这个 QuerySet,包括 filter()、order_by(),甚至额外的调用来 annotate()。
combining multiple aggregations(结合多个聚合)
将多个聚合与 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 # wrong
>>> q[0].store__count
6 # wrong
对于大多数聚合,没有办法避免这个问题,但是,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 属性。
Joins and aggregates(连接和聚合)
到目前为止,我们已经处理了属于被查询模型的字段的聚合。然而,有时您想要聚合的值属于与您正在查询模型的相关模型。
当指定要在聚合函数中聚合的字段时,Django 允许您使用与在过滤器中引用相关字段时相同的双下划线表示法。Django 将处理检索和聚合相关值所需的任何表连接。
例如,要查找每个书店提供的图书的价格范围,可以使用 annotation:
>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))
这告诉 Django 检索 Store 模型,( 通过多对多关系 ) 与 Book 模型连接,并在 Book 模型的 price 字段上聚合,以生成最小值和最大值。
同样的规则也适用于 aggregate() 子句。如果你想知道在任何一家书店出售的任何一本书的最低和最高价格,你可以使用以下组合:
>>> Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))
连接链可以根据您的需要加深,例如,要提取任何一本书最年轻作者的年龄,您可以发出以下查询:
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))
Following relationships backwards(反向关系)
与跨越关系的查找类似,模型或与您正在查询的模型相关的模型字段上的 aggregations 和 annotations 可以包括遍历“反向”关系。这里也使用了相关模型的小写名称和双下划线。
例如,我们可以要求所有 publishers ,用他们各自的图书总库存计数器进行 annotated ( 注意我们如何使用 book 指定 Publisher->Book reverse foreign key hop ):(没看懂)
>>> from django.db.models import Avg, Count, Min, Sum
>>> Publisher.objects.annotate(Count('book'))
QuerySet 结果集中的每个 Publisher 都有一个名为 book_count 的额外属性。
我们还可以向每一位 publisher 索要其中一本最古老的书:
>>> Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate'))
生成的 dictionary 将有一个名为 oldest_pubdate 的键。如果没有指定这样的别名,它将是相当长的 book_pubdate_min。
这不仅适用于外键,也适用于多对多关系。例如,我们可以求每个作者,考虑到作者 ( 合作 ) 所写的所有书籍,用总页数来注释( 注意我们如何使用 book 来指定 Author—> Book reverse many-to-many hop ):(没看懂)
>>> Author.objects.annotate(total_pages=Sum('book__pages'))
QuerySet 结果集中的每个作者都有一个名为 total_pages 的额外属性。如果没有指定这样的别名,那么它将是相当长的book_pages_sum。
或求我们存档的所有作者所写书籍的平均评分:
>>> Author.objects.aggregate(average_rating=Avg('book__rating'))
生成的 dictionary 有一个名为 average_rating 的键。如果没有指定这样的别名,它将是相当长的 book_rating_avg。
Aggregations and other QuerySet clauses(聚合和其他 QuerySet 字句)
filter() 和 exclude()
Aggregates 还可以加入 filters。应用于普通模型字段的任何 filter() ( 或 exclude() ) 都会对考虑用于聚合的对象进行约束。
当与 annotate() 子句一起使用时,filter 的作用是约束其计算 annotation 的对象。例如,您可以使用以下查询生成以 Django 开头的所有图书的带注释的列表:
>>> from django.db.models import Avg, Count
>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))
Filtering on annotations
还可以过滤带注释的值。注释的别名可以在 filter() 和 exclude() 子句中使用,方法与任何其他模型字段相同。
例如,要生成包含多个作者的图书列表,可以发出以下查询:
>>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)
该查询生成一个带注释的结果集,然后基于该注释生成一个过滤器。
如果需要两个带有两个单独过滤器的注释,可以对任何聚合使用 filter 参数。例如,生成一个拥有高评价书籍数量的作者列表:
>>> highly_rated = Count('books', filter=Q(books__rating__gte=7))
>>> Author.objects.annotate(num_books=Count('books'), highly_rated_books=highly_rated)
结果集中的每个作者都有 num_books 和 highly_rated_books 属性。
注解:在 filter 和 QuerySet.filter() 之间进行选择:避免在单个 annotation 或 aggregation 中使用 filter 参数。使用 QuerySet.filter() 来排除行更有效。Aggregation filter argument 仅在对具有不同条件的相同关系使用两个或多个聚合时才有用。
Order of annotate() and filter() clauses (annotate() 和 filter() 子句的顺序)
在开发包含 annotate() 和 filter() 子句的复杂查询时,要特别注意子句应用于 QuerySet 的顺序。
当将 annotate() 子句应用于查询时,将根据查询的状态计算注释(即先 filter,再 annotate),直到请求注释为止。这意味着 filter() 和 annotate() 不是可交换操作。
给出:
- Publisher A has two books with ratings 4 and 5.(PA 有两本评分分别是4和5的书)
- Publisher B has two books with ratings 1 and 4.(PB有两本评分分别是1和4的书)
- Publisher C has one book with rating 1.(PC有一本评分为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,因此不包括PublisherC。
在第一个查询中,annotation 在 filter 之前,所以 filter 对 annotation 没有影响。distinct=True 是避免查询错误所必需的。
第二个查询计算每个出版商的评分超过3.0的图书数量。filter 位于 annotation 之前,因此 annotation 在计算注释时约束考虑的对象。
下面是 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() 子句的一部分定义的任何别名。
例如,要按对本书有贡献的作者的数量排序,可以使用以下查询:
>>> 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() 子句之后,则需要显式地包含聚合列。
Interaction with default ordering or order_by() (与默认 ordering 或 order_by() 交互)
在 queryset 的 order_by() 部分中提到的字段 ( 或者在模型的默认顺序中使用的字段 )即使在 values() 调用中没有指定它们但仍在选择输出数据时使用。这些额外的字段用于将“like”结果分组在一起,它们可以使其他相同的结果行看起来是分开的。这一点在计算时尤其明显。(没咋看懂,看例子吧)
举个例子,假设你有这样一个模型:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
data = models.IntegerField()
class Meta:
ordering = ["name"]
这里的重要部分是 name 字段是默认排序字段。如果你想计算每个不同的数据值出现的次数,你可以这样做:
# Warning: not quite correct!
Item.objects.values("data").annotate(Count("id"))
…它将根据项目对象的公共 data 值对其进行分组,然后计算每个组中的 id 值的数量。只是它不会完全奏效。默认的按 name 排序也将在分组中发挥作用,因此该查询将按不同的( data、name)对进行分组,这不是您想要的。相反,您应该构造这个queryset:
Item.objects.values("data").annotate(Count("id")).order_by()
…清除查询中的任何顺序。您还可以按数据排序,而不会产生任何有害影响,因为数据已经在查询中发挥了作用。
这种行为和在 distinct() 的 queryset 文档提到的及一般规则是一样的:通常你不想额外列成为结果集的一部分,因此清除 ordering,或至少确保它是仅限于在 values() 调用的字段。
注解:
您可能会问Django为什么不为您删除无关的列。主要原因是 distinct() 和其他一些地方:Django不会删除您指定的顺序约束( 和我们不能改变其他方法的行为,因为这样会违反我们的 API的稳定性 政策 )。
Aggregating annotations(聚合注释)
您还可以根据注释的结果生成聚合。定义 aggregate() 子句时,提供的聚合可以引用查询中作为 annotate() 子句的一部分定义的任何别名。
例如,如果您想计算每本书的平均作者数量,那么您首先使用作者数量对图书集进行注释,然后聚合该作者数量,并引用 annotation 字段:
>>> from django.db.models import Avg, Count
>>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))
{'num_authors__avg': 1.66}