用实例详解Python中的Django框架中prefetch_related()函数对数据库查询的优化


Posted in Python onApril 01, 2015

实例的背景说明

假定一个个人信息系统,需要记录系统中各个人的故乡、居住地、以及到过的城市。数据库设计如下:

用实例详解Python中的Django框架中prefetch_related()函数对数据库查询的优化

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.lastname

注1:创建的app名为“QSOptimize”

注2:为了简化起见,`qsoptimize_province` 表中只有2条数据:湖北省和广东省,`qsoptimize_city`表中只有三条数据:武汉市、十堰市和广州市

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()应该是这么做:
 

>>> zhangs = Person.objects.prefetch_related('visitation').get(firstname=u"张",lastname=u"三")
>>> for city in zhangs.visitation.all() :
...  print city
...

上述代码触发的SQL查询如下:
 

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`,
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`
WHERE (`QSOptimize_person`.`lastname` = '三' AND `QSOptimize_person`.`firstname` = '张');
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1);

第一条SQL查询仅仅是获取张三的Person对象,第二条比较关键,它选取关系表`QSOptimize_person_visitation`中`person_id`为张三的行,然后和`city`表内联(INNER JOIN 也叫等值连接)得到结果表。
 

+----+-----------+----------+-------------+-----------+
| id | firstname | lastname | hometown_id | living_id |
+----+-----------+----------+-------------+-----------+
| 1 | 张    | 三    |      3 |     1 |
+----+-----------+----------+-------------+-----------+
1 row in set (0.00 sec)
 
+-----------------------+----+-----------+-------------+
| _prefetch_related_val | id | name   | province_id |
+-----------------------+----+-----------+-------------+
|           1 | 1 | 武汉市  |      1 |
|           1 | 2 | 广州市  |      2 |
|           1 | 3 | 十堰市  |      1 |
+-----------------------+----+-----------+-------------+
3 rows in set (0.00 sec)

显然张三武汉、广州、十堰都去过。

又或者,我们要获得湖北的所有城市名,可以这样:
 

>>> hb = Province.objects.prefetch_related('city_set').get(name__iexact=u"湖北省")
>>> for city in hb.city_set.all():
...  city.name
...

触发的SQL查询:
 

SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`name` LIKE '湖北省' ;
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
WHERE `QSOptimize_city`.`province_id` IN (1);

得到的表:
 

+----+-----------+
| id | name   |
+----+-----------+
| 1 | 湖北省  |
+----+-----------+
1 row in set (0.00 sec)
 
+----+-----------+-------------+
| id | name   | province_id |
+----+-----------+-------------+
| 1 | 武汉市  |      1 |
| 3 | 十堰市  |      1 |
+----+-----------+-------------+
2 rows in set (0.00 sec)

我们可以看见,prefetch使用的是 IN 语句实现的。这样,在QuerySet中的对象数量过多的时候,根据数据库特性的不同有可能造成性能问题。

 
使用方法
*lookups 参数

prefetch_related()在Django < 1.7 只有这一种用法。和select_related()一样,prefetch_related()也支持深度查询,例如要获得所有姓张的人去过的省:
 

>>> zhangs = Person.objects.prefetch_related('visitation__province').filter(firstname__iexact=u'张')
>>> for i in zhangs:
...  for city in i.visitation.all():
...   print city.province
...

触发的SQL:
 

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`,
`QSOptimize_person`.`lastname`, `QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`
WHERE `QSOptimize_person`.`firstname` LIKE '张' ;
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id` FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1, 4);
 
SELECT `QSOptimize_province`.`id`, `QSOptimize_province`.`name`
FROM `QSOptimize_province`
WHERE `QSOptimize_province`.`id` IN (1, 2);

获得的结果:
 

+----+-----------+----------+-------------+-----------+
| id | firstname | lastname | hometown_id | living_id |
+----+-----------+----------+-------------+-----------+
| 1 | 张    | 三    |      3 |     1 |
| 4 | 张    | 六    |      2 |     2 |
+----+-----------+----------+-------------+-----------+
2 rows in set (0.00 sec)
 
+-----------------------+----+-----------+-------------+
| _prefetch_related_val | id | name   | province_id |
+-----------------------+----+-----------+-------------+
|           1 | 1 | 武汉市  |      1 |
|           1 | 2 | 广州市  |      2 |
|           4 | 2 | 广州市  |      2 |
|           1 | 3 | 十堰市  |      1 |
+-----------------------+----+-----------+-------------+
4 rows in set (0.00 sec)
 
+----+-----------+
| id | name   |
+----+-----------+
| 1 | 湖北省  |
| 2 | 广东省  |
+----+-----------+
2 rows in set (0.00 sec)

值得一提的是,链式prefetch_related会将这些查询添加起来,就像1.7中的select_related那样。

要注意的是,在使用QuerySet的时候,一旦在链式操作中改变了数据库请求,之前用prefetch_related缓存的数据将会被忽略掉。这会导致Django重新请求数据库来获得相应的数据,从而造成性能问题。这里提到的改变数据库请求指各种filter()、exclude()等等最终会改变SQL代码的操作。而all()并不会改变最终的数据库请求,因此是不会导致重新请求数据库的。

举个例子,要获取所有人访问过的城市中带有“市”字的城市,这样做会导致大量的SQL查询:
 

plist = Person.objects.prefetch_related('visitation')
[p.visitation.filter(name__icontains=u"市") for p in plist]

因为数据库中有4人,导致了2+4次SQL查询:
 

SELECT `QSOptimize_person`.`id`, `QSOptimize_person`.`firstname`, `QSOptimize_person`.`lastname`,
`QSOptimize_person`.`hometown_id`, `QSOptimize_person`.`living_id`
FROM `QSOptimize_person`;
 
SELECT (`QSOptimize_person_visitation`.`person_id`) AS `_prefetch_related_val`, `QSOptimize_city`.`id`,
`QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE `QSOptimize_person_visitation`.`person_id` IN (1, 2, 3, 4);
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE(`QSOptimize_person_visitation`.`person_id` = 1 AND `QSOptimize_city`.`name` LIKE '%市%' );
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE (`QSOptimize_person_visitation`.`person_id` = 2 AND `QSOptimize_city`.`name` LIKE '%市%' );
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE (`QSOptimize_person_visitation`.`person_id` = 3 AND `QSOptimize_city`.`name` LIKE '%市%' );
 
SELECT `QSOptimize_city`.`id`, `QSOptimize_city`.`name`, `QSOptimize_city`.`province_id`
FROM `QSOptimize_city`
INNER JOIN `QSOptimize_person_visitation` ON (`QSOptimize_city`.`id` = `QSOptimize_person_visitation`.`city_id`)
WHERE (`QSOptimize_person_visitation`.`person_id` = 4 AND `QSOptimize_city`.`name` LIKE '%市%' );

详细分析一下这些请求事件。

众所周知,QuerySet是lazy的,要用的时候才会去访问数据库。运行到第二行Python代码时,for循环将plist看做iterator,这会触发数据库查询。最初的两次SQL查询就是prefetch_related导致的。

虽然已经查询结果中包含所有所需的city的信息,但因为在循环体中对Person.visitation进行了filter操作,这显然改变了数据库请求。因此这些操作会忽略掉之前缓存到的数据,重新进行SQL查询。

但是如果有这样的需求了应该怎么办呢?在Django >= 1.7,可以通过下一节的Prefetch对象来实现,如果你的环境是Django < 1.7,可以在Python中完成这部分操作。
 

plist = Person.objects.prefetch_related('visitation')
[[city for city in p.visitation.all() if u"市" in city.name] for p in plist]

Prefetch 对象

在Django >= 1.7,可以用Prefetch对象来控制prefetch_related函数的行为。

注:由于我没有安装1.7版本的Django环境,本节内容是参考Django文档写的,没有进行实际的测试。

Prefetch对象的特征:

  •     一个Prefetch对象只能指定一项prefetch操作。
  •     Prefetch对象对字段指定的方式和prefetch_related中的参数相同,都是通过双下划线连接的字段名完成的。
  •     可以通过 queryset 参数手动指定prefetch使用的QuerySet。
  •     可以通过 to_attr 参数指定prefetch到的属性名。
  •     Prefetch对象和字符串形式指定的lookups参数可以混用。

继续上面的例子,获取所有人访问过的城市中带有“武”字和“州”的城市:
 

wus = City.objects.filter(name__icontains = u"武")
zhous = City.objects.filter(name__icontains = u"州")
plist = Person.objects.prefetch_related(
  Prefetch('visitation', queryset = wus, to_attr = "wu_city"),
  Prefetch('visitation', queryset = zhous, to_attr = "zhou_city"),)
[p.wu_city for p in plist]
[p.zhou_city for p in plist]

注:这段代码没有在实际环境中测试过,若有不正确的地方请指正。

顺带一提,Prefetch对象和字符串参数可以混用。
None

可以通过传入一个None来清空之前的prefetch_related。就像这样:

>>> prefetch_cleared_qset = qset.prefetch_related(None)

小结

  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。
Python 相关文章推荐
Python比较文件夹比另一同名文件夹多出的文件并复制出来的方法
Mar 05 Python
使用Python的PEAK来适配协议的教程
Apr 14 Python
python追加元素到列表的方法
Jul 28 Python
Python连接数据库学习之DB-API详解
Feb 07 Python
使用Python实现windows下的抓包与解析
Jan 15 Python
Django实现支付宝付款和微信支付的示例代码
Jul 25 Python
Python Selenium 之数据驱动测试的实现
Aug 01 Python
Python内建序列通用操作6种实现方法
Mar 26 Python
完美解决Pycharm中matplotlib画图中文乱码问题
Jan 11 Python
python spilt()分隔字符串的实现示例
May 21 Python
浅谈Python数学建模之数据导入
Jun 23 Python
关于Python OS模块常用文件/目录函数详解
Jul 01 Python
Python的Django框架中的select_related函数对QuerySet 查询的优化
Apr 01 #Python
简单的Python2.7编程初学经验总结
Apr 01 #Python
极简的Python入门指引
Apr 01 #Python
分析在Python中何种情况下需要使用断言
Apr 01 #Python
用Python制作简单的朴素基数估计器的教程
Apr 01 #Python
简单的编程0基础下Python入门指引
Apr 01 #Python
python查找目录下指定扩展名的文件实例
Apr 01 #Python
You might like
Smarty中的注释和截断功能介绍
2015/04/09 PHP
composer.lock文件的作用
2016/02/03 PHP
PHP之将POST数据转化为字符串的实现代码
2016/11/03 PHP
Zend Framework使用Zend_Loader组件动态加载文件和类用法详解
2016/12/09 PHP
thinkPHP5.0框架安装教程
2017/03/25 PHP
PHP读取word文档的方法分析【基于COM组件】
2017/08/01 PHP
js 多浏览器分别判断代码
2010/04/01 Javascript
客户端js判断文件类型和文件大小即限制上传大小
2013/11/20 Javascript
JS脚本defer的作用示例介绍
2014/01/02 Javascript
javascript实现的平方米、亩、公顷单位换算小程序
2014/08/11 Javascript
js实现鼠标滑过文字链接色彩变化的效果
2015/05/06 Javascript
7个jQuery最佳实践
2016/01/12 Javascript
BootStrap Table对前台页面表格的支持实例讲解
2016/12/22 Javascript
jQuery is not defined 错误原因与解决方法小结
2017/03/19 Javascript
vue项目前端知识点整理【收藏】
2019/05/13 Javascript
JavaScript内置对象math,global功能与用法实例分析
2019/06/10 Javascript
vue+element导航栏高亮显示的解决方式
2019/11/12 Javascript
微信小程序事件流原理解析
2019/11/27 Javascript
解决Vue-cli无法编译es6的问题
2020/10/30 Javascript
python实现分析apache和nginx日志文件并输出访客ip列表的方法
2015/04/04 Python
用pickle存储Python的原生对象方法
2017/04/28 Python
vue.js实现输入框输入值内容实时响应变化示例
2018/07/07 Python
对python实现二维函数高次拟合的示例详解
2018/12/29 Python
解决PDF 转图片时丢文字的一种可能方式
2021/03/04 Python
CSS3制作半透明边框(Facebox)类似渐变
2012/12/09 HTML / CSS
东方电视购物:东方CJ
2016/10/12 全球购物
Tea Collection官网:一家位于旧金山的童装公司
2020/08/07 全球购物
门卫岗位职责
2013/11/15 职场文书
心理健康课教学反思
2014/02/13 职场文书
综治维稳工作汇报
2014/10/27 职场文书
2014年人力资源工作总结
2014/11/19 职场文书
认真学习保证书
2015/02/26 职场文书
房屋租赁意向书范本
2015/05/09 职场文书
葬礼主持词
2015/07/02 职场文书
Go 语言中 20 个占位符的整理
2021/10/16 Golang
Python中npy和mat文件的保存与读取
2022/04/24 Python