Django框架詳細介紹---ORM相關操作---select_related和prefetch_related函數對 QuerySet 查詢的優化
Django的 select_related 和 prefetch_related 函數對 QuerySet 查詢的優化
引言
在數據庫存在外鍵的其情況下,使用select_related()和prefetch_related()很大程度上減少對數據庫的請求次數以提高性能
1.實例準備
模型:
from django.db import models # Create your models here. # 書 class Book(models.Model): title = models.CharField(max_length=32) publish_date= models.DateField(auto_now_add=True) price = models.DecimalField(max_digits=5, decimal_places=2) memo = models.TextField(null=True) # 創建外鍵,關聯publish publisher = models.ForeignKey(to="Publisher", on_delete=models.CASCADE, related_name=‘books‘) # 創建多對多關聯author author = models.ManyToManyField(to="Author") def __str__(self): return self.title # 出版社 class Publisher(models.Model): name = models.CharField(max_length=32) city = models.CharField(max_length=32) def __str__(self): return self.name # 作者 class Author(models.Model): name = models.CharField(max_length=32) age= models.IntegerField() phone = models.CharField(max_length=11) detail = models.OneToOneField(to="AuthorDetail", on_delete=models.CASCADE) def __str__(self): return self.name # 作者詳情 class AuthorDetail(models.Model): addr = models.CharField(max_length=64) email = models.EmailField()
1.select_related()
對於一對一字段(OneToOneField)和外鍵字段(ForeignKey)可以使用select_releated來優化QuerySet查詢
示例:
1)未調用該方法時,查詢所有書籍以及所在的出版社
book_obj = models.Book.objects.all() # 打印所有書籍以及所在的出版社 for book in book_obj: print(book.publisher)
輸出的日誌信息
(0.000) SELECT @@SQL_AUTO_IS_NULL; args=None (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None (0.001) SELECT `app_book`.`id`, `app_book`.`title`, `app_book`.`publish_date`, `app_book`.`price`, `app_book`.`memo`, `app_book`.`publisher_id` FROM `app_book`; args=() 南方傳媒出版社 長沙電視出版社 長沙電視出版社 清華大學出版社 人民郵電出版社 (0.001) SELECT VERSION(); args=None 南方傳媒出版社 (0.001) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 1; args=(1,) (0.001) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 2; args=(2,) (0.000) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 2; args=(2,) (0.000) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 4; args=(4,) (0.001) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 3; args=(3,) (0.000) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city` FROM `app_publisher` WHERE `app_publisher`.`id` = 1; args=(1,) Process finished with exit code 0
2)調用該方法時,查詢所有書籍以及所在的出版社
book_obj = models.Book.objects.select_related().all() for book in book_obj: print(book.publisher)
輸出的日誌信息
(0.001) SELECT @@SQL_AUTO_IS_NULL; args=None (0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None (0.001) SELECT `app_book`.`id`, `app_book`.`title`, `app_book`.`publish_date`,
`app_book`.`price`, `app_book`.`memo`, `app_book`.`publisher_id`, `app_publisher`.`id`,
`app_publisher`.`name`, `app_publisher`.`city` FROM `app_book` INNER JOIN `app_publisher`
ON (`app_book`.`publisher_id` = `app_publisher`.`id`); args=() 南方傳媒出版社 長沙電視出版社 長沙電視出版社 清華大學出版社 人民郵電出版社 南方傳媒出版社 Process finished with exit code 0
結論:
在對QuerySet使用select_related()函數前,會出現線性的SQL查詢,如果存在n個查詢對象,每個對象存在k個外鍵字段時,就會出現n*k + 1 次SQL查詢
在對QuerySet使用select_related()函數後,Django會在進行第一次查詢時獲取相應外鍵對應的對象,從而在之後需要的時候不必再查詢數據庫,只需要進行一次查詢
3)相關參數
*fields參數,select_related() 接受可變長參數,每個參數是需要獲取的外鍵(父表的內容)的字段名,以及外鍵的外鍵的字段名、外鍵的外鍵的外鍵等,若要選擇外鍵的外鍵需要使用兩個下劃線“__”來連接
示例:通過外鍵獲取書籍的出版社的詳細地址
obj = models.Book.objects.select_related(‘publisher__detail‘).get(title=‘武林傳奇‘)
print(obj.publisher.detail)
print(obj.publisher)
輸出的日誌信息
(0.000) SELECT @@SQL_AUTO_IS_NULL; args=None
(0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
(0.000) SELECT VERSION(); args=None
(0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
(0.001) SELECT `app_book`.`id`, `app_book`.`title`, `app_book`.`publish_date`, `app_book`.`price`, `app_book`.`memo`, `app_book`.`publisher_id`, `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city`, `app_publisher`.`detail_id`, `app_publisherdetail`.`id`, `app_publisherdetail`.`addr`, `app_publisherdetail`.`create_date` FROM `app_book` INNER JOIN `app_publisher` ON (`app_book`.`publisher_id` = `app_publisher`.`id`) INNER JOIN `app_publisherdetail` ON (`app_publisher`.`detail_id` = `app_publisherdetail`.`id`) WHERE `app_book`.`title` = ‘武林傳奇‘; args=(‘武林傳奇‘,)
長沙南區
長沙電視出版社
結論:
Django使用了2次 INNER JOIN 來完成請求,獲得了publisher表和publisherdetail表的內容並添加到結果表的相應列,再次調用這兩個對象的時候也不必再次進行SQL查詢;反之如是未指定的外鍵則會進行SQL查詢,且深度更深,查詢次數更多
depth參數,select_related() 接受depth參數,depth參數可以確定select_related的深度,Django會遞歸遍歷指定深度內的所有的OneToOneField和ForeignKey
無參數,select_related() 也可以不加參數,這樣表示要求Django盡可能深的select_related
總結:
1.select_related主要針一對一和多對一關系進行優化
2.select_related使用SQL的JOIN語句進行優化,通過減少SQL查詢的次數來進行優化、提高性能
3.可以通過可變長參數指定需要select_related的字段名。也可以通過使用雙下劃線“__”連接字段名來實現指定的遞歸查詢。沒有指定的字段不會緩存,沒有指定的深度不會緩存,如果要訪問的話Django會再次進行SQL查詢
4.也可以通過depth參數指定遞歸的深度,Django會自動緩存指定深度內所有的字段。如果要訪問指定深度外的字段,Django會再次進行SQL查詢
5.也接受無參數的調用,Django會盡可能深的遞歸查詢所有的字段。但註意有Django遞歸的限制和性能的浪費
6.Django >= 1.7,鏈式調用的select_related相當於使用可變長參數。Django < 1.7,鏈式調用會導致前邊的select_related失效,只保留最後一個
2.prefetch_releated()
對於多對多字段(ManyToManyField)和一對多(ForeignKey)字段,可使用prefetch_related()來進行優化,它和select_releated的設計目的相似,都是為了減少SQL的查詢次數,但實現方式不一樣,後者是通過JOIN語句在SQL查詢內解決問題。但由於JOIN語句過於冗長,不適合解決多對多關系,會導致SQL語句運行時間的增加和內存占用增加
prefetch_releated的解決方法是分別查詢每個表,然後用Python處理他們之間的關系
示例:
book_obj = models.Book.objects.prefetch_related(‘publisher‘).get(title=‘七俠五義‘) print(book_obj.publisher.detail)
輸出的日誌信息
(0.000) SELECT @@SQL_AUTO_IS_NULL; args=None (0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None (0.000) SELECT VERSION(); args=None (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None (0.000) SELECT `app_book`.`id`, `app_book`.`title`, `app_book`.`publish_date`, `app_book`.`price`, `app_book`.`memo`, `app_book`.`publisher_id` FROM `app_book` WHERE `app_book`.`title` = ‘七俠五義‘; args=(‘七俠五義‘,) (0.000) SELECT `app_publisher`.`id`, `app_publisher`.`name`, `app_publisher`.`city`, `app_publisher`.`detail_id` FROM `app_publisher` WHERE `app_publisher`.`id` IN (1); args=(1,) (0.001) SELECT `app_publisherdetail`.`id`, `app_publisherdetail`.`addr`, `app_publisherdetail`.`create_date` FROM `app_publisherdetail` WHERE `app_publisherdetail`.`id` = 1; args=(1,) 廣州天河區 Process finished with exit code 0
結論:
可以看出,第一條查詢語句拿到的要操作的對象,關鍵在於第二條拿到了要查詢結果的條件也就是ID,第三行通過第二行拿到的ID進行結果查詢
從第二條查詢語句中可看出,可以看見,prefetch使用的是 IN 語句實現的。這樣,在QuerySet中的對象數量過多的時候,根據數據庫特性的不同有可能造成性能問題
*lookups參數,prefetch_related()在Django < 1.7 只有這一種用法。和select_related()一樣,prefetch_related()也支持深度查詢
要註意的是,在使用QuerySet的時候,一旦在鏈式操作中改變了數據庫請求,之前用prefetch_related緩存的數據將會被忽略掉。這會導致Django重新請求數據庫來獲得相應的數據,從而造成性能問題。這裏提到的改變數據庫請求指各種filter()、exclude()等等最終會改變SQL代碼的操作。而all()並不會改變最終的數據庫請求,因此是不會導致重新請求數據庫的
總結:
1.prefetch_related主要針一對多和多對多關系進行優化 2.prefetch_related通過分別獲取各個表的內容,然後用Python處理他們之間的關系來進行優化 3.可以通過可變長參數指定需要select_related的字段名。指定方式和特征與select_related是相同的 4.在Django >= 1.7可以通過Prefetch對象來實現復雜查詢,但低版本的Django好像只能自己實現 5.作為prefetch_related的參數,Prefetch對象和字符串可以混用 6.prefetch_related的鏈式調用會將對應的prefetch添加進去,而非替換,似乎沒有基於不同版本上區別 7.可以通過傳入None來清空之前的prefetch_related
歸納:
1.因為select_related()總是在單次SQL查詢中解決問題,而prefetch_related()會對每個相關表進行SQL查詢,因此select_related()的效率通常比後者高
2.鑒於第一條,盡可能的用select_related()解決問題。只有在select_related()不能解決問題的時候再去想prefetch_related()
3.可以在一個QuerySet中同時使用select_related()和prefetch_related(),從而減少SQL查詢的次數
4.只有prefetch_related()之前的select_related()是有效的,之後的將會被無視掉
Django框架詳細介紹---ORM相關操作---select_related和prefetch_related函數對 QuerySet 查詢的優化