用实例详解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的ORM框架中的SQLAlchemy库的映射关系
Apr 25 Python
教大家使用Python SqlAlchemy
Feb 12 Python
python实现xlsx文件分析详解
Jan 02 Python
pandas获取groupby分组里最大值所在的行方法
Apr 20 Python
Python利用heapq实现一个优先级队列的方法
Feb 03 Python
Django框架模板文件使用及模板文件加载顺序分析
May 23 Python
Python实现K折交叉验证法的方法步骤
Jul 11 Python
python爬虫 模拟登录人人网过程解析
Jul 31 Python
python GUI库图形界面开发之PyQt5控件数据拖曳Drag与Drop详细使用方法与实例
Feb 27 Python
浅谈多卡服务器下隐藏部分 GPU 和 TensorFlow 的显存使用设置
Jun 30 Python
通俗易懂了解Python装饰器原理
Sep 17 Python
Python 操作pdf pdfplumber读取PDF写入Exce
Aug 14 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
PHP ? EasyUI DataGrid 资料取的方式介绍
2012/11/07 PHP
十幅图告诉你什么是PHP引用
2015/02/22 PHP
php正则表达式获取内容所有链接
2015/07/24 PHP
centos 7.2下搭建LNMP环境教程
2016/11/20 PHP
JavaScript 学习笔记一些小技巧
2010/03/28 Javascript
两个listbox实现选项的添加删除和搜索
2013/03/01 Javascript
jquery 实现二级/三级/多级联动菜单的思路及代码
2013/04/08 Javascript
固定表格行列(expression)在IE下适用
2013/07/25 Javascript
简介JavaScript中的setHours()方法的使用
2015/06/11 Javascript
javascript实现类似于新浪微博搜索框弹出效果的方法
2015/07/27 Javascript
web 屏蔽BackSpace键实例代码
2016/12/24 Javascript
jQuery事件与动画基础详解
2017/02/23 Javascript
JavaScript实现替换字符串中最后一个字符的方法
2017/03/07 Javascript
vue axios 表单提交上传图片的实例
2018/03/16 Javascript
webpack分离css单独打包的方法
2018/06/12 Javascript
boostrap模态框二次弹出清空原有内容的方法
2018/08/10 Javascript
[02:21]DOTA2英雄基础教程 蝙蝠骑士
2013/12/16 DOTA
浅谈Python 集合(set)类型的操作——并交差
2016/06/30 Python
Python多维/嵌套字典数据无限遍历的实现
2016/11/04 Python
Python初学时购物车程序练习实例(推荐)
2017/08/08 Python
Django之Mode的外键自关联和引用未定义的Model方法
2018/12/15 Python
python2.7使用plotly绘制本地散点图和折线图
2019/04/02 Python
Python Multiprocessing多进程 使用tqdm显示进度条的实现
2019/08/13 Python
Python Web框架之Django框架Model基础详解
2019/08/16 Python
Python Django 添加首页尾页上一页下一页代码实例
2019/08/21 Python
Python3将数据保存为txt文件的方法
2019/09/12 Python
python3 求约数的实例
2019/12/05 Python
python分布式计算dispy的使用详解
2019/12/22 Python
OpenCV图片漫画效果的实现示例
2020/08/18 Python
Python实现壁纸下载与轮换
2020/10/19 Python
详解selenium + chromedriver 被反爬的解决方法
2020/10/28 Python
对于没有初始化的变量的初始值可以作怎样的假定
2014/10/12 面试题
营业员演讲稿
2013/12/30 职场文书
采购意向书范本
2014/03/31 职场文书
美丽家庭事迹材料
2014/05/03 职场文书
交通安全教育主题班会
2015/08/12 职场文书