Python的Django框架中的select_related函数对QuerySet 查询的优化


Posted in Python onApril 01, 2015

1. 实例的背景说明

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

Python的Django框架中的select_related函数对QuerySet 查询的优化

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`表中只有三条数据:武汉市、十堰市和广州市
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并不知道你实际要用的字段有哪些,所以会把所有的字段都抓进来,从而会造成不必要的浪费而影响性能。

 
小结

  1.     select_related主要针一对一和多对一关系进行优化。
  2.     select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  3.     可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
  4.     也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
  5.     也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。
  6.     Django >= 1.7,链式调用的select_related相当于使用可变长参数。Django < 1.7,链式调用会导致前边的select_related失效,只保留最后一个。

Python 相关文章推荐
python下载文件记录黑名单的实现代码
Oct 24 Python
异步任务队列Celery在Django中的使用方法
Jun 07 Python
Python脚本按照当前日期创建多级目录
Mar 01 Python
python解压TAR文件至指定文件夹的实例
Jun 10 Python
Python中变量的输入输出实例代码详解
Jul 28 Python
Django 1.10以上版本 url 配置注意事项详解
Aug 05 Python
Django模板导入母版继承和自定义返回Html片段过程解析
Sep 18 Python
Python Sphinx使用实例及问题解决
Jan 17 Python
利用Python自动化操作AutoCAD的实现
Apr 01 Python
ffmpeg+Python实现B站MP4格式音频与视频的合并示例代码
Oct 21 Python
selenium如何定位span元素的实现
Jan 13 Python
python time.strptime格式化实例详解
Feb 03 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
Python利用多进程将大量数据放入有限内存的教程
Apr 01 #Python
You might like
mysql5的sql文件导入到mysql4的方法
2008/10/19 PHP
两个强悍的php 图像处理类1
2009/06/15 PHP
php获取url字符串截取路径的文件名和扩展名的函数
2010/01/22 PHP
PHP引用符&amp;的用法详细解析
2013/08/22 PHP
js计数器代码
2006/11/04 Javascript
jquery 可排列的表实现代码
2009/11/13 Javascript
如何让div span等元素能响应键盘事件操作指南
2012/11/13 Javascript
jquery如何改变html标签的样式(两种实现方法)
2013/01/16 Javascript
JS在IE下缺少标识符的错误
2014/07/23 Javascript
微信中一些常用的js方法汇总
2015/03/12 Javascript
JavaScript中利用Array filter() 方法压缩稀疏数组
2018/02/24 Javascript
Angular4 反向代理Details实践
2018/05/30 Javascript
深入理解与使用keep-alive(配合router-view缓存整个路由页面)
2018/09/25 Javascript
vue使用v-for实现hover点击效果
2018/09/29 Javascript
layui select 禁止点击的实现方法
2019/09/05 Javascript
细述Javascript的加法运算符的具体使用
2019/10/18 Javascript
jQuery插件simplePagination的使用方法示例
2020/04/28 jQuery
JavaScript实现手风琴效果
2021/02/18 Javascript
[01:35]2014DOTA2西雅图邀请赛 专访狐狸妈青春献给刀塔
2014/07/08 DOTA
Python实现基于多线程、多用户的FTP服务器与客户端功能完整实例
2017/08/18 Python
python中urlparse模块介绍与使用示例
2017/11/19 Python
TensorFlow 实战之实现卷积神经网络的实例讲解
2018/02/26 Python
对python 调用类属性的方法详解
2019/07/02 Python
Python内置方法实现字符串的秘钥加解密(推荐)
2019/12/09 Python
python实现用类读取文件数据并计算矩形面积
2020/01/18 Python
python pandas.DataFrame.loc函数使用详解
2020/03/26 Python
Python 通过爬虫实现GitHub网页的模拟登录的示例代码
2020/08/17 Python
Python实现曲线拟合的最小二乘法
2021/02/19 Python
加拿大最大的相机店:Henry’s
2017/05/17 全球购物
国家机关领导干部民主生活会对照检查材料思想汇报
2014/09/17 职场文书
2014年业务工作总结
2014/11/17 职场文书
2014年行政助理工作总结
2014/11/19 职场文书
Python合并多张图片成PDF
2021/06/09 Python
Canvas绘制像素风图片的示例代码
2021/09/25 HTML / CSS
alibaba seata服务端具体实现
2022/02/24 Java/Android
《月歌。》宣布制作10周年纪念剧场版《RABBITS KINGDOM THE MOVIE》
2022/04/02 日漫