在数据库有外键的时候,使用 select_related() 和 prefetch_related() 可以很好的减少数据库请求的次数,从而提高性能。本文通过一个简单的例子详解这两个函数的作用。虽然QuerySet的文档中已经详细说明了,但本文试图从QuerySet触发的SQL语句来分析工作方式,从而进一步了解Django具体的运作方式。
本来打算写成一篇单独的文章的,但是写完select_related()之后发现长度已经有点长了,所以还是写成系列,大概在两到三篇。整个完成之后将会在这里添加上其他文章的链接。
假定一个个人信息系统,需要记录系统中各个人的故乡、居住地、以及到过的城市。数据库设计如下:
Models.py 内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
.
lastname
|
注1:创建的app名为“QSOptimize”
注2:为了简化起见,qsoptimize_province
表中只有2条数据:湖北省和广东省,qsoptimize_city
表中只有三条数据:武汉市、十堰市和广州市
对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化
在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。以上例说明,如果我们需要打印数据库中的所有市及其所属省份,最直接的做法是:
1
2
3
4
|
>>>
citys
=
City
.
objects
.
all
(
)
>>>
for
c
in
citys
:
.
.
.
print
c
.
province
.
.
.
|
这样会导致线性的SQL查询,如果对象数量n太多,每个对象中有k个外键字段的话,就会导致n*k+1次SQL查询。在本例中,因为有3个city对象就导致了4次SQL查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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()函数:
1
2
3
4
|
>>>
citys
=
City
.
objects
.
select_related
(
)
.
all
(
)
>>>
for
c
in
citys
:
.
.
.
print
c
.
province
.
.
.
|
就只有一次SQL查询,显然大大减少了SQL查询的次数:
1
2
3
4
|
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查询得到的结果如下:
1
2
3
4
5
6
7
8
|
+
--
--
+
--
--
--
--
--
-
+
--
--
--
--
--
--
-
+
--
--
+
--
--
--
--
--
-
+
|
id
|
name
|
province_id
|
id
|
name
|
+
--
--
+
--
--
--
--
--
-
+
--
--
--
--
--
--
-
+
--
--
+
--
--
--
--
--
-
+
|
1
|
武汉市
|
1
|
1
|
湖北省
|
|
2
|
广州市
|
2
|
2
|
广东省
|
|
3
|
十堰市
|
1
|
1
|
湖北省
|
+
--
--
+
--
--
--
--
--
-
+
--
--
--
--
--
--
-
+
--
--
+
--
--
--
--
--
-
+
3
rows
in
set
(
0.00
sec
)
|
select_related() 接受可变长参数,每个参数是需要获取的外键(父表的内容)的字段名,以及外键的外键的字段名、外键的外键的外键…。若要选择外键的外键需要使用两个下划线“__”来连接。
例如我们要获得张三的现居省份,可以用如下方式:
1
2
|
>>>
zhangs
=
Person
.
objects
.
select_related
(
'living__province'
)
.
get
(
firstname
=
u
"张"
,
lastname
=
u
"三"
)
>>>
zhangs
.
living
.
province
|
触发的SQL查询如下:
1
2
3
4
5
6
7
8
|
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查询。
1
2
3
4
5
6
|
+
--
--
+
--
--
--
--
--
-
+
--
--
--
--
--
+
--
--
--
--
--
--
-
+
--
--
--
--
--
-
+
--
--
+
--
--
--
--
--
-
+
--
--
--
--
--
--
-
+
--
--
+
--
--
--
--
--
-
+
|
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查询了:
1
|
>>>
zhangs
.
hometown
.
province
|
1
2
3
4
5
6
7
8
|
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以前你只能这样做:
1
2
3
|
>>>
zhangs
=
Person
.
objects
.
select_related
(
'hometown__province'
,
'living__province'
)
.
get
(
firstname
=
u
"张"
,
lastname
=
u
"三"
)
>>>
zhangs
.
hometown
.
province
>>>
zhangs
.
living
.
province
|
但是1.7及以上版本,你可以像和queryset的其他函数一样进行链式操作:
1
2
3
|
>>>
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查询。
select_related() 接受depth参数,depth参数可以确定select_related的深度。Django会递归遍历指定深度内的所有的OneToOneField和ForeignKey。以本例说明:
1
|
>>>
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”三”)。但要注意两点: