天天看點

執行個體具體解釋Django的 select_related 和 prefetch_related 函數對 QuerySet 查詢的優化(二)

這是本系列的第二篇。内容是 prefetch_related() 函數的用途、實作途徑、以及用法。

本系列的第一篇在這裡

第三篇在這裡

對于多對多字段(ManyToManyField)和一對多字段。能夠使用prefetch_related()來進行優化。也許你會說,沒有一個叫OneToManyField的東西啊。實際上 。ForeignKey就是一個多對一的字段。而被ForeignKey關聯的字段就是一對多字段了。

prefetch_related()和select_related()的設計目的非常相似,都是為了降低SQL查詢的數量。可是實作的方式不一樣。

後者是通過JOIN語句,在SQL查詢内解決這個問題。可是對于多對多關系。使用SQL語句解決就顯得有些不太明智。由于JOIN得到的表将會非常長。會導緻SQL語句執行時間的添加和記憶體占用的添加。

若有n個對象,每一個對象的多對多字段相應Mi條,就會生成Σ(n)Mi 行的結果表。

prefetch_related()的解決方法是。分别查詢每一個表,然後用Python處理他們之間的關系。

繼續以上邊的樣例進行說明。假設我們要獲得張三全部去過的城市。使用prefetch_related()應該是這麼做:

上述代碼觸發的SQL查詢例如以下:

第一條SQL查詢不過擷取張三的Person對象,第二條比較關鍵,它選取關系表`QSOptimize_person_visitation`中`person_id`為張三的行,然後和`city`表内聯(INNER JOIN 也叫等值連接配接)得到結果表。

顯然張三武漢、廣州、十堰都去過。

又或者。我們要獲得湖北的全部城市名,能夠這樣:

觸發的SQL查詢:

得到的表:

我們能夠看見,prefetch使用的是 IN 語句實作的。這樣。在QuerySet中的對象數量過多的時候。依據資料庫特性的不同有可能造成性能問題。

prefetch_related()在Django < 1.7 僅僅有這一種使用方法。和select_related()一樣,prefetch_related()也支援深度查詢,比如要獲得全部姓張的人去過的省:

觸發的SQL:

獲得的結果:

值得一提的是,鍊式prefetch_related會将這些查詢加入起來,就像1.7中的select_related那樣。

要注意的是,在使用QuerySet的時候,一旦在鍊式操作中改變了資料庫請求,之前用prefetch_related緩存的資料将會被忽略掉。這會導緻Django又一次請求資料庫來獲得對應的資料,進而造成性能問題。這裡提到的改變資料庫請求指各種filter()、exclude()等等終于會改變SQL代碼的操作。而all()并不會改變終于的資料庫請求,是以是不會導緻又一次請求資料庫的。

舉個樣例,要擷取全部人訪問過的城市中帶有“市”字的城市,這樣做會導緻大量的SQL查詢:

由于資料庫中有4人,導緻了2+4次SQL查詢:

具體分析一下這些請求事件。

衆所周知。QuerySet是lazy的,要用的時候才會去訪問資料庫。

執行到第二行Python代碼時。for循環将plist看做iterator,這會觸發資料庫查詢。最初的兩次SQL查詢就是prefetch_related導緻的。

盡管已經查詢結果中包括全部所需的city的資訊。但由于在循環體中對Person.visitation進行了filter操作,這顯然改變了資料庫請求。

是以這些操作會忽略掉之前緩存到的資料,又一次進行SQL查詢。

可是假設有這種需求了應該怎麼辦呢?在Django >= 1.7,能夠通過下一節的Prefetch對象來實作,假設你的環境是Django < 1.7。能夠在Python中完畢這部分操作。

在Django >= 1.7,能夠用Prefetch對象來控制prefetch_related函數的行為。

注:因為我沒有安裝1.7版本号的Django環境。本節内容是參考Django文檔寫的,沒有進行實際的測試。

Prefetch對象的特征:

一個Prefetch對象僅僅能指定一項prefetch操作。

Prefetch對象對字段指定的方式和prefetch_related中的參數同樣。都是通過雙下劃線連接配接的字段名完畢的。

能夠通過 queryset 參數手動指定prefetch使用的QuerySet。

能夠通過 to_attr 參數指定prefetch到的屬性名。

Prefetch對象和字元串形式指定的lookups參數能夠混用。

繼續上面的樣例,擷取全部人訪問過的城市中帶有“武”字和“州”的城市:

注:這段代碼沒有在實際環境中測試過。若有不對的地方請指正。

順帶一提,Prefetch對象和字元串參數能夠混用。

能夠通過傳入一個None來清空之前的prefetch_related。就像這樣:

prefetch_related主要針一對多和多對多關系進行優化。

prefetch_related通過分别擷取各個表的内容,然後用Python處理他們之間的關系來進行優化。

能夠通過可變長參數指定須要select_related的字段名。指定方式和特征與select_related是同樣的。

在Django >= 1.7能夠通過Prefetch對象來實作複雜查詢,但低版本号的Django好像僅僅能自己實作。

作為prefetch_related的參數。Prefetch對象和字元串能夠混用。

prefetch_related的鍊式調用會将相應的prefetch加入進去,而非替換,似乎沒有基于不同版本号上差别。

能夠通過傳入None來清空之前的prefetch_related。