django聚合查询

聚合

Django 数据库抽象API 描述了使用Django 查询来增删查改单个对象的方法。然而,有时候你须要获取的值须要根据一组对象聚合后才能获得。这份指南描述经过Django 查询来生成和返回聚合值的方法。html

整篇指南咱们都将引用如下模型。这些模型用来记录多个网上书店的库存。python

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

速查表

急着用吗?如下是在上述模型的基础上,进行通常的聚合查询的方法:git

# 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 >>> 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 # many-to-many relationship backward # 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 [<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 

在查询集上生成聚合

Django提供了两种生成聚合的方法。第一种方法是从整个查询集生成统计值。好比,你想要计算全部在售书的平均价钱。Django的查询语法提供了一种方式描述全部图书的集合。sql

>>> Book.objects.all() 

咱们须要在QuerySet.对象上计算出总价格。这能够经过在QuerySet后面附加aggregate() 子句来完成。数据库

>>> from django.db.models import Avg >>> Book.objects.all().aggregate(Avg('price')) {'price__avg': 34.35} 

all()在这里是多余的,因此能够简化为:django

>>> Book.objects.aggregate(Avg('price')) {'price__avg': 34.35} 

aggregate()子句的参数描述了咱们想要计算的聚合值,在这个例子中,是Book 模型中price字段的平均值。查询集参考中列出了聚合函数的列表。api

aggregate()QuerySet 的一个终止子句,意思是说,它返回一个包含一些键值对的字典。键的名称是聚合值的标识符,值是计算出来的聚合值。键的名称是按照字段和聚合函数的名称自动生成出来的。若是你想要为聚合值指定一个名称,能够向聚合子句提供它。ide

>>> 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.中每本书里的这种关系。ui

逐个对象的汇总结果能够由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() 不是一个终止子句。annotate()子句的返回结果是一个查询集 (QuerySet);这个 QuerySet能够用任何QuerySet方法进行修改,包括 filter()order_by(), 甚至是再次应用annotate()

有任何疑问的话,请检查 SQL query!

要想弄清楚你的查询到底发生了什么,能够考虑检查你QuerySet的 query 属性。

例如,在annotate() 中混入多个聚合将会得出错误的结果,由于多个表上作了交叉链接,致使了多余的行聚合。

链接和聚合

至此,咱们已经了解了做用于单种模型实例的聚合操做, 可是有时,你也想对所查询对象的关联对象进行聚合。

在聚合函数中指定聚合字段时,Django 容许你使用一样的 双下划线 表示关联关系,而后 Django 在就会处理要读取的关联表,并获得关联对象的聚合。

例如,要查找每一个商店提供的图书的价格范围,您可使用注释:

>>> from django.db.models import Max, Min >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) 

这段代码告诉 Django 获取书店模型,并链接(经过多对多关系)图书模型,而后对每本书的价格进行聚合,得出最小值和最大值。

一样的规则也用于  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')) 

(每一个返回的QuerySet中的Author 都有一个额外的属性叫作total_pages若是没有指定这样的别名,它会更长一些,像 book__pages__sum。)

或者查询全部图书的平均评分,这些图书由咱们存档过的做者所写:

>>> Author.objects.aggregate(average_rating=Avg('book__rating')) 

(返回的字典会包含一个键叫作'average__rating'若是没有指定这样的别名,它会更长一些,像'book__rating__avg'。)

聚合和其余查询集子句

filter() and 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() 子句做用于某个查询时,要根据查询的状态才能得出注解值,而状态由 annotate() 位置所决定。以这就致使filter() 和 annotate() 不能交换顺序,下面两个查询就是不一样的:

>>> Publisher.objects.annotate(num_books=Count('book')).filter(book__rating__gt=3.0) 

另外一个查询:

>>> Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book')) 

两个查询都返回了至少出版了一本好书(评分大于 3 分)的出版商。 可是第一个查询的注解包含其该出版商发行的全部图书的总数;而第二个查询的注解只包含出版过好书的出版商的所发行的图书总数。 在第一个查询中,注解在过滤器以前,因此过滤器对注解没有影响。 在第二个查询中,过滤器在注解以前,因此,在计算注解值时,过滤器就限制了参与运算的对象的范围。

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

在这个例子中,做者会按名称分组,因此你只能获得某个惟一的做者分组的注解值。 这意味着若是你有两个做者同名,那么他们本来各自的查询结果将被合并到同一个结果中;两个做者的全部评分都将被计算为一个平均分。

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() 子句以后,你须要显式地包含聚合列。

与默认排序交换或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 stability 原则)。

聚合注解

你也能够在注解的结果上生成聚合。 当你定义一个  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} 

 

相关文章
相关标签/搜索