這是本系列的第二篇。内容是 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。