Django 3.2.3 Aggregation 聚合

译者注: 本文翻译自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语句!
为了理解查询时发生了什么,考虑查看QuerySetquery属性。


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

(每个结果QuerySetPublisher有一个额外的属性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_bookshighly_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}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容