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} 

 

相關文章
相關標籤/搜索