聚合查询

本文的例子基于以下模型:

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

推荐阅读更多精彩内容