django的使用3-聚合

[TOC]

聚合

Django 数据库抽象 API 描述了使用 Django queries 来增删查改单个对象的方法。 然而,有时候你要获取的值需要根据一组对象聚合后才能得到。

模型示例: 作者,出版社,书,商店。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

常见的聚合查询

  • expressions

  • output_field

  • filter

  • Avg

  • Count

  • Max

  • Min

  • Sum

  • Variance: 返回给定表达式中数据的方差

  • StdDev: 返回给定表达式中数据的标准差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 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

aggregate

Django 提供了两种生成聚合的方法。第一种方法是从整个 QuerySet 生成汇总值。比如你想要计算所有在售书的平均价格。Django 的查询语法提供了一种用来描述所有图书集合的方法:

1
>>> Book.objects.all()

可以通过在 QuerySet 后添加 aggregate() 子句来计算 QuerySet 对象的汇总值。

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

本例中的 all() 是多余的,所以可以简化成这样的:

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

传递给 aggregate() 的参数描述了我们想要计算的聚合值。在这个例子里,要计算的就是 Book 模型上的 price 字段的平均值。

aggregate()QuerySet 的一个终端子句,使用后将返回“名称-值”的字典,其中“名称”就是聚合值的标志,“值”就是计算出的聚合结果。“名称”是根据字段名和聚合函数而自动生成的。如果你想指定一个聚合值的名称,你可以在指定聚合子句的时候提供指定的名称:

1
2
>>> Book.objects.aggregate(average_price=Avg('price'))
{'average_price': 34.35}

如果你想生成更多的聚合内容,你需要在 aggregate() 子句中加入其它参数即可。所以,如果我们也想知道所有书中最高和最低的价格,我们可以写这样的查询:

1
2
3
>>> 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() 的每一个参数描述了一个要计算的聚合。比如,注解(annotate)所有书的所有作者:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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() 一样,注解的名称是根据聚合函数和被聚合的字段名自动生成的。当你在指定注解的时候,你可以通过提供一个别名重写这个默认名。

1
2
3
4
5
>>> 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()` 进行额外调用

组合多个聚合

使用 annotate() 组合多个聚合将产生错误的结果,因为它使用连接(joins)而不是子查询:

1
2
3
4
5
6
7
8
9
10
>>> 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
>>> q[0].store__count
6

对大部分聚合来说,没办法避免这个问题,但是,Count聚合可以使用 distinct 参数来避免:

1
2
3
4
5
>>> q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True))
>>> q[0].authors__count
2
>>> q[0].store__count
3

连接(Joins)和聚合

有时候想聚合的值属于你正在查询模型的关联模型。在聚合函数里面指定聚合的字段时,Django 允许你在过滤相关字段的时候使用相同的双下划线表示法。Django 将处理任何需要检索和聚合的关联值的表连接(table joins)。

比如,要寻找每个书店提供的书籍价格区间,你可以使用这个注解(annotation):

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

这告诉 Django 去检索 Store 模型,连接(通过多对多关系) Book 模型,并且聚合书籍模型的价格字段来获取最大最小值。

相同规则应用于 aggregate() 从句。如果你想知道任何店铺正在销售的任何书籍的最低最高价,你可以使用这个聚合:

1
>>> Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))

Join 链可以根据你的需求尽可能深。比如,要提取所出售的书籍中最年轻的作者年龄,你可以写这样的查询:

1
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))

参考: