译者注: 本文翻译自Django的官方文档2.1版本。因此只适用于2.1版本和其它未改动本章节的版本。
文档尽量全文翻译,但是以下情况下可能没有翻译:章节的开头和结尾没有实质内容的引导语和结束语,原文中的所有标题、一些斜体和粗体的术语或链接,代码中的部分注释,一些和Django本身关系不太大的技术细节。
文中的部分英文斜体和粗体在原文中是转到相关章节的超链接,但译文中没做链接,后期翻译的章节足够时会逐渐添加。
受译者水平所限,文中一些术语和专业的表达可能有错误,欢迎通过简信或评论提出错误和修改意见。
祝各位Django开发者和爱好者阅读使用愉快!
3.2.3 Aggregation
我们将引用下面的模型贯穿整个教程。这些教程用来追踪一系列线上书店的库存:
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, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
registered_users = models.PositiveIntegerField()
Cheat sheet
赶时间?这里是常用的聚合查询,假设已有上面的模型:
# 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('books')).order_by('-num_books')[:5]
>>> pubs[0].num_books
1323
Generating aggregates over a 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 reference中查看。
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')}
Generating aggregates for each item in a QuerySet
第二种产生总计值的方式是为QuerySet
的每个对象产生一个独立的总结。比如,如果你获得了一个书的列表,你可能想知道每本书分别有多少个作者。每本书有一个和作者的多对多关系;我们为QuerySet
中的每本书统计关系。
每个对象的总结可以使用annotate()
语句产生。每个annotate()
参数描述了一个将要计算的聚合。比如,为注解每本书的作者数:
# Build an annotated queryset
>>> from django.db.models import Count
>>> q = Book.objects.annotate(Count('authors'))
# Interrogate the first objects 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
不同于aggregate()
, annotate()
不是一个终端语句。annotate()
的输出语句是一个QuerySet
:这个QuerySet
可用任意QuerySet
操作修改,包括filter()
,order_by()
,甚至再调用annotate()
。
Combining multiple aggregations
使用annotate()
组合多个聚合会产生错误的结果因为使用的是联表而不是子查询:
>>> book = Book.objects.first()
>>> book.author.count()
2
>>> book.store_set.count()
3
>>> q = Book.objects.annotate(Count('authors'), Count('store'))
>>> q[0].authors__count
6
>>> q[0].stores__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
属性。
Joins and aggregates
到目前为止,我们已经处理了查询模型的聚合字段。然而,有时你想聚合的模型的值是查询的模型相关的。
当指定聚合函数的待聚合字段时,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=('books__price'))
联表链可以如你所需的深。比如为了抽取在售书的最年轻的作者的年龄,可以使用查询语句:
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))
Following relationships backwards
类似于Lookups that span relationships,模型或查询的关联模型的字段的聚合和注解可以包含反向关系。使用的也是小写的模型名和双下划线。
比如说,我们可以请求所有的出版社,注解他们分别的书的数量():
>>> 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'。)
这不仅适用于外键。他也适用于多对多关系。比如,我们可以请求每个作者,注解考虑所有有该作者的书的页数总数:
>>> 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'。)
Aggregations and other QuerySet
clauses
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'))
Filtering on annotations
注解值也可以过滤。注解的别名可以用在filter()
和exclude()
语句中,就想别的模型字段。
比如,为生成一个不止一个作者的书的列表,可以使用下面的语句:
>>> Book.objects.annotate(num_author=Count('authors')).filter(num_author__gt=1)
这个查询集会产生一个注解结果集,然后产生基于注解的过滤器。
如果你需要在两个分别的过滤器上的两个注解,你可以将filter
参数和别的聚合一起使用。比如,产生一个高排名书的作者的列表:
>>> highly_rated = Count('book', filter=Q(books__rating__gte=7))
>>> Author.objects.annotate(num__books=Count('books'), highly_rated_books=highly_rated)
结果集中的每个Author
会有num_books
和highly_rated_books
属性。
Choosing between filter
and QuerySet.filter()
避免在单个注解或聚合中使用filter
参数。使用QuerySet.filter()
来过滤行更有效率。只有当囊括不同条件的两个以上的聚合时,聚合的filter
参数才是有用的
于是,filter
参数增加到了聚合里。
Order of annotate()
and filter()
clauses
当开发一个复杂的包含annotate()
和filter()
的查询语句时,注意应用在QuerySet
上的语句顺序。
当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.nu_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被排除了。
在第一次查询中,注解先于过滤器,因此过滤器对出版社没有影响,distincr=True
用来避免query bug。
第二个查询语句计算了每个出版社有排名大于3的书的数量。过滤器先于注解,因此过滤器在计算注解时限制了考虑的对象范围。
这里是另一个关于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'))
在这个例子中,作者会按照名字分组,所以你会得到一个每个唯一作者名的注解结果。这意味着如果你有两个作者的名字相同,他们的结果会合并到输出结果的同一个条目下;平均值会变成这两个作者的书的平均值。
Order of annotate()
and values()
clause
就像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()
查询集中order_by()
提及的字段在选择输出数据时会用到,即使没有在调用values()
时提及。这些额外的字段会用来分组结果,并且他们会使得结果列分开。当计数时这一点尤为明显。
为了举例,假设你有一个这样的模型:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
date = 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 stability原则)。
Aggregating annotations
也可以聚合注解的结果集。当你定义一个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}