1. 程式人生 > >Django框架詳細介紹---ORM相關操作---select_related和prefetch_related函數對 QuerySet 查詢的優化

Django框架詳細介紹---ORM相關操作---select_related和prefetch_related函數對 QuerySet 查詢的優化

ger AS AI 出版社 獲得 har 有效 object 庫存

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 查詢的優化