轉 實例詳解Django的 select_related 和 prefetch_related 函數對 QuerySet 查詢的優化(一)

轉載自https://blog.csdn.net/cugbabybear/article/details/38342793數據庫

 

 

在數據庫有外鍵的時候,使用 select_related() 和 prefetch_related() 能夠很好的減小數據庫請求的次數,從而提升性能。本文經過一個簡單的例子詳解這兩個函數的做用。雖然QuerySet的文檔中已經詳細說明了,但本文試圖從QuerySet觸發的SQL語句來分析工做方式,從而進一步瞭解Django具體的運做方式。django

 

這是本系列的第一篇, 內容是 select_related()  函數的用途、實現途徑、以及使用方法緩存

第二篇在這裏,內容是 prefetch_related() 函數的用途、實現途徑、以及使用方法app

第三篇在這裏,用幾個實例來講明一些複雜查詢最佳實踐。數據庫設計

 

1. 實例的背景說明
假定一個我的信息系統,須要記錄系統中各我的的故鄉、居住地、以及到過的城市。數據庫設計以下:函數

 

Models.py 內容以下:性能

from django.db import models

class Province(models.Model):
name = models.CharField(max_length=10)
def __unicode__(self):
return self.name

class City(models.Model):
name = models.CharField(max_length=5)
province = models.ForeignKey(Province)
def __unicode__(self):
return self.name

class Person(models.Model):
firstname = models.CharField(max_length=10)
lastname = models.CharField(max_length=10)
visitation = models.ManyToManyField(City, related_name = "visitor")
hometown = models.ForeignKey(City, related_name = "birth")
living = models.ForeignKey(City, related_name = "citizen")
def __unicode__(self):
return self.firstname + self.lastnamefetch

注1:建立的app名爲「QSOptimize」優化

注2:爲了簡化起見,`qsoptimize_province` 表中只有2條數據:湖北省和廣東省,`qsoptimize_city`表中只有三條數據:武漢市、十堰市和廣州市.net

 

2. select_related()
對於一對一字段(OneToOneField)和外鍵字段(ForeignKey),可使用select_related 來對QuerySet進行優化

 

做用和方法

在對QuerySet使用select_related()函數後,Django會獲取相應外鍵對應的對象,從而在以後須要的時候沒必要再查詢數據庫了。以上例說明,若是咱們須要打印數據庫中的全部市及其所屬省份,最直接的作法是:

>>> citys = City.objects.all()
>>> for c in citys:
... print c.province
...
這樣會致使線性的SQL查詢,若是對象數量n太多,每一個對象中有k個外鍵字段的話,就會致使n*k+1次SQL查詢。在本例中,由於有3個city對象就致使了4次SQL查詢:
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 1 ;

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 2 ;

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 1 ;
注:這裏的SQL語句是直接從Django的logger:‘django.db.backends’輸出出來的

 

 

若是咱們使用select_related()函數:

>>> citys = City.objects.select_related().all()
>>> for c in citys:
...   print c.province
...
就只有一次SQL查詢,顯然大大減小了SQL查詢的次數:

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`,
`QSOptimize_city`.`province_id`, `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM`QSOptimize_city`
INNER JOIN `QSOptimize_province` ON (`QSOptimize_city`.`province_id` = `QSOptimize_province`.`id`) ;
這裏咱們能夠看到,Django使用了INNER JOIN來得到省份的信息。順便一提這條SQL查詢獲得的結果以下:

+----+-----------+-------------+----+-----------+
| id | name | province_id | id | name |
+----+-----------+-------------+----+-----------+
| 1 | 武漢市 | 1 | 1 | 湖北省 |
| 2 | 廣州市 | 2 | 2 | 廣東省 |
| 3 | 十堰市 | 1 | 1 | 湖北省 |
+----+-----------+-------------+----+-----------+
3 rows in set (0.00 sec)


使用方法
函數支持以下三種用法:
*fields 參數
select_related() 接受可變長參數,每一個參數是須要獲取的外鍵(父表的內容)的字段名,以及外鍵的外鍵的字段名、外鍵的外鍵的外鍵...。若要選擇外鍵的外鍵須要使用兩個下劃線「__」來鏈接。

例如咱們要得到張三的現居省份,能夠用以下方式:

>>> zhangs = Person.objects.select_related('living__province').get(firstname=u"張",lastname=u"三")
>>> zhangs.living.province
觸發的SQL查詢以下:

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`,
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`,
`QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`, `QSOptimize_province`.`id`,
`QSOptimize_province`.`name`
FROM `QSOptimize_person`
INNER JOIN `QSOptimize_city` ON (`QSOptimize_person`.`living_id` = `QSOptimize_city`.`id`)
INNER JOIN `QSOptimize_province` ON (`QSOptimize_city`.`province_id` = `QSOptimize_province`.`id`)
WHERE (`QSOptimize_person`.`lastname` = '三'  AND `QSOptimize_person`.`firstname` = '張' );
能夠看到,Django使用了2次 INNER JOIN 來完成請求,得到了city表和province表的內容並添加到結果表的相應列,這樣在調用 zhangs.living的時候也沒必要再次進行SQL查詢。

+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
| id | firstname | lastname | hometown_id | living_id | id | name | province_id | id | name |
+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
| 1 | 張 | 三 | 3 | 1 | 1 | 武漢市 | 1 | 1 | 湖北省 |
+----+-----------+----------+-------------+-----------+----+-----------+-------------+----+-----------+
1 row in set (0.00 sec)

 


然而,未指定的外鍵則不會被添加到結果中。這時候若是須要獲取張三的故鄉就會進行SQL查詢了:

>>> zhangs.hometown.province

SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`,
`QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
WHERE `QSOptimize_city`.`id` = 3 ;

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` = 1
同時,若是不指定外鍵,就會進行兩次查詢。若是深度更深,查詢的次數更多。

 

 

值得一提的是,從Django 1.7開始,select_related()函數的做用方式改變了。在本例中,若是要同時得到張三的故鄉和現居地的省份,在1.7之前你只能這樣作:
>>> zhangs = Person.objects.select_related('hometown__province','living__province').get(firstname=u"張",lastname=u"三")
>>> zhangs.hometown.province
>>> zhangs.living.province
可是1.7及以上版本,你能夠像和queryset的其餘函數同樣進行鏈式操做:

>>> zhangs = Person.objects.select_related('hometown__province').select_related('living__province').get(firstname=u"張",lastname=u"三")
>>> zhangs.hometown.province
>>> zhangs.living.province
若是你在1.7如下版本這樣作了,你只會得到最後一個操做的結果,在本例中就是隻有現居地而沒有故鄉。在你打印故鄉省份的時候就會形成兩次SQL查詢。

depth 參數
select_related() 接受depth參數,depth參數能夠肯定select_related的深度。Django會遞歸遍歷指定深度內的全部的OneToOneField和ForeignKey。以本例說明:

>>> zhangs = Person.objects.select_related(depth = d)
d=1  至關於 select_related('hometown','living')

d=2  至關於 select_related('hometown__province','living__province')

無參數 
select_related() 也能夠不加參數,這樣表示要求Django儘量深的select_related。例如:zhangs = Person.objects.select_related().get(firstname=u"張",lastname=u"三")。但要注意兩點:

Django自己內置一個上限,對於特別複雜的表關係,Django可能在你不知道的某處跳出遞歸,從而與你想的作法不同。具體限制是怎麼工做的我表示不清楚。
Django並不知道你實際要用的字段有哪些,因此會把全部的字段都抓進來,從而會形成沒必要要的浪費而影響性能。
 

小結select_related主要針一對一和多對一關係進行優化。select_related使用SQL的JOIN語句進行優化,經過減小SQL查詢的次數來進行優化、提升性能。能夠經過可變長參數指定須要select_related的字段名。也能夠經過使用雙下劃線「__」鏈接字段名來實現指定的遞歸查詢。沒有指定的字段不會緩存,沒有指定的深度不會緩存,若是要訪問的話Django會再次進行SQL查詢。也能夠經過depth參數指定遞歸的深度,Django會自動緩存指定深度內全部的字段。若是要訪問指定深度外的字段,Django會再次進行SQL查詢。也接受無參數的調用,Django會盡量深的遞歸查詢全部的字段。但注意有Django遞歸的限制和性能的浪費。Django >= 1.7,鏈式調用的select_related至關於使用可變長參數。Django < 1.7,鏈式調用會致使前邊的select_related失效,只保留最後一個。--------------------- 做者:CuGBabyBeaR 來源:CSDN 原文:https://blog.csdn.net/cugbabybear/article/details/38342793 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索