本文的例子基于以下模型:
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)
在查询集上生成聚合(aggregate)
Django 提供了两种生成聚合的方法。第一种方法是从整个查询集 (QuerySet)生成统计值。
比如,你想要计算所有在售书的平均价钱。Django 的查询语法可以这样描述所有图书的集合:
b = Book.objects.all()
我们可以通过在 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 字段的平均值。
aggregate( )是 QuerySet 的一个终止子句,意思是说,它返回一个包含一些键值对的字典。键的名称是聚合值的标识符,值是计算出来的聚合值。键的名称是按照字段和聚合函数的名称自动生成出来的。
参见上例,该聚合返回了一个字典:
{'price__avg': 34.35}
如果你想要为聚合值指定一个键值名称,可以这样:
>>> 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')}
为查询集的每一项生成聚合(annotate)
生成汇总值的第二种方法,是为 QuerySet 中每一个对象都生成一个独立的汇总值。
比如,如果你在检索一系列图书,你可能想知道每一本书有多少作者参与,书和作者是多对多关系,我们可以通过聚合来完成这种查询。
逐个对象的汇总结果可以由 annotate() 子句生成。当 annotate() 子句被指定之后,QuerySet 中的每个对象都会被注上特定的值。
这些注解的语法都和 aggregate() 子句所使用的相同。annotate() 的每个参数都描述了将要被计算的聚合。比如,给图书添加作者数量的注解:
from django.db.models import Count
# 为 Book 的每一个对象计算 authors 的数量
# 并为 Book 的每一个对象添加一个authors__count 属性,其值为计算得出的 authors 的数量
q = Book.objects.annotate(Count('authors'))
# 这时 q 仍然是一个查询集
>>> q[0]
<Book: 图书1>
>>> q[0].authors__count
2
和使用 aggregate() 一样,注解的名称也是根据聚合函数的名称和聚合字段的名称自动生成的。如果你要指定注解名,可以这样:
>>> q = Book.objects.annotate(num_authors=Count('authors'))
>>> q[0].num_authors
2
与 aggregate() 不同的是, annotate() 不是一个终止子句。annotate() 子句的返回结果仍然是一个查询集 (QuerySet)。这个查询集可以用任何 QuerySet 方法进行修改,包括 filter(), order_by(),甚至是再次使用 annotate()。
连接和聚合
现在,我们已经了解了作用于单种模型实例的聚合操作, 但是有时,你也想对查询对象的关联对象进行聚合(可以理解为从子表查询母表)。
在聚合函数中指定聚合字段时,Django 允许你使用同样的 双下划线 表示关联关系,然后 Django 在就会处理要读取的关联表,并得到关联对象的聚合。
例如,要查找每个商店提供的图书的价格范围,您可以这样:
from django.db.models import Max, Min
# 计算每个书店所售卖图书的最高价格和最低价格
s = Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))
# 第一家书店所有书中的最低价
>>> s[0].min_price
Decimal('10.00')
# 第一家书店所有书中的最高价
>>> s[0].max_price
Decimal('30.00')
同样的规则也用于 aggregate() 子句。下例将计算所有书店中最便宜的书和最贵的书价格分别是多少:
>>> Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))
{'min_price': Decimal('10.00'), 'max_price': Decimal('180.00')}
关系链可以按你的要求一直延伸。 例如,想得到所有作者当中最小的年龄是多少,就可以这样写:
# 通过 Store 的外键查到 Book,再通过 Book 的外键查到 Author
# 最后查询 Author 的 age 属性
Store.objects.aggregate(youngest_age=Min('books__authors__age'))
遵循反向关系
和跨关系查找的方法类似,作用在你所查询的模型的关联模型或者字段上的聚合和注解可以遍历"反转"关系(可以理解为从母表查询子表)。关联模型的小写名称和双下划线也用在这里。
例如,我们可以查询所有出版商,并注上它们一共出了多少本书:
from django.db.models import Count, Min, Sum, Avg
# 每一个 Publisher 对象都会包含一个额外的 book__count 属性
>>> p = Publisher.objects.annotate(Count('book'))
>>> p[0].book__count
1
>>> p[1].book__count
3
我们也可以按照每个出版商,查询所有图书中最旧的那本,可以用 子表模型名的小写 + 双下划线 + 字段名(如:book__pubdate) 来查询:
# 在这里 Publisher 是母表,Book 是子表,通过母表查询子表
>>> Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate'))
{'oldest_pubdate': datetime.date(2017, 4, 1)}
我们也可以查询某个出版社最旧的那本书,用 annotate() 方法即可:
>>> p = Publisher.objects.annotate(oldest_pubdate=Min('book__pubdate'))
>>> p[0].oldest_pubdate
datetime.date(2017, 4, 1)
这不仅仅可以应用在外键上面。还可以用到多对多关系上。
例如,我们可以查询每个作者,注上它写的所有书(包括合著的书)一共有多少页:
>>> a = Author.objects.annotate(total_pages=Sum('book__pages'))
>>> a[0].total_pages
290
或者查询所有图书的平均评分:
>>> Author.objects.aggregate(average_rating=Avg('book__rating'))
{'average_rating': 3.75}
注意:这里有个坑,因为 Author 和 Book 是多对多关系,当出现某本书有多个作者时,上诉方法会把一本书的评分统计多次,然后按作者“人次”来计算平均数。
我们再用出版社来计算所有图书的平均评分:
>>> Publisher.objects.aggregate(average_rating=Avg('book__rating'))
{'average_rating': 3.8333333333333335}
由于 Publisher 和 Book 是一对多的关系,所以这里的方法计算的是 各图书评分之和 / 图书总数 。
聚合和其他查询集子句
1. filter() 和 exclude()
聚合也可以在过滤器中使用。 我们可以使用 filter() (或 exclude() ) 都会对聚合涉及的对象进行筛选,方法是先用 filter() (或 exclude() ) 生成一个查询集,然后再对其进行聚合。
例如,你想得到每本以 "Django" 为书名开头的图书作者的总数:
>>> b = Book.objects.filter(name__startswith="图书").annotate(num_authors=Count('authors'))
>>> b[0].num_authors
2
同理,也可以使用 aggregate() 方法,例如,你可以算出所有以 "Django" 为书名开头的图书平均价格:
>>> Book.objects.filter(name__startswith="Django").aggregate(Avg('price'))
2.对注解过滤
注解值也可以被过滤。 我们可以为查询集添加注解值,然后使用 filter() 或 exclude() 对注解值进行筛选。
例如,要得到作者的不止一个的图书,可以用:
Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)
order_by()
注解可以用来做为排序项。 在你定义 order_by() 子句时,可以使用注解值作为参数。
例如,根据一本图书作者数量的多少进行排序:
>>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')
values()
values() 方法返回的查询集中的对象会变成一个字典而不是一个模型实例,values() 方法也可以和聚合结合使用。
# 先产生一个对象为字典的查询集
# 在进行聚合,附注上新的键:average_rating
>>> Author.objects.values('name').annotate(average_rating=Avg('book__rating'))
>>> a[0]
{'average_rating': 3.65, 'name': '作者1'}
聚合注解
你也可以在注解的结果上生成聚合。
例如,如果你想计算每本书平均有几个作者,可以先用作者总数注解图书查询集,然后再聚合作者总数,引入注解字段:
>>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))
{'num_authors__avg': 1.66}