在数据库有外键的时候,使用 select_related() 和 prefetch_related() 可以很好的减少数据库请求的次数,从而提高性能。本文通过一个简单的例子从QuerySet触发的SQL语句来分析工作方式,从而进一步了解Django具体的运作方式。

0.初始化

打开log调试,在setting中加入

LOGGING = {'version':1,'disable_existing_loggers':False,'handlers':{'console':{'level':'DEBUG','class':'logging.StreamHandler',},},'loggers':{'django.db.backends':{'handlers':['console'],'propagate':True,'level':'DEBUG'},}
}
1.实例的背景说明

假定一个人有多本书籍,多个人可能拥有同名的一本书籍,一个人有一本最喜爱的书籍,每本书籍对应一个出版社,一个出版社对应多本书

class Publish(models.Model):name = models.CharField(verbose_name="出版社", max_length=20)class Meta:db_table = "PUBLISH"def __str__(self):return self.nameclass Book(models.Model):book_name = models.CharField(verbose_name="书名", max_length=20)publish = models.ForeignKey(Publish, verbose_name="出版社", on_delete=models.CASCADE)class Meta:db_table = "BOOK"def __str__(self):return self.book_nameclass Person(models.Model):first_name = models.CharField(max_length=10)last_name = models.CharField(max_length=10)book = models.ManyToManyField(Book, verbose_name="拥有书籍", related_name="book")favorite_book = models.ForeignKey(Book, verbose_name="最喜爱的一本书",on_delete=models.CASCADE, related_name='favorite_book')class Meta:db_table = "PERSON"def __str__(self):return self.first_name + self.last_name

为了简化,我们只添加两个出版社,每个出版社有两本书
人民日报出版社: 钢铁是怎样炼成的,西游记
北京大学出版社:巴黎圣母院,水浒传

2.select_related()

对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化

作用和方法
在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。以上例说明,如果我们需要打印数据库中的书籍及其所属出版社,最直接的做法是:

books = Book.objects.all()
for book in books:print(book.publish)

这样会导致线性的SQL查询,如果对象数量n太多,每个对象中有k个外键字段的话,就会导致n*k+1次SQL查询。在本例中,因为有4个publish对象就导致了5次SQL查询:

SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK`; SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`id` = 1; SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`id` = 1; SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`id` = 2; SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`id` = 2;

如果我们使用select_related()函数:

books = Book.objects.select_related().all()
for book in books:print(book.publish)

就只有一次SQL查询,大大减少了SQL查询的次数:

SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id`, `PUBLISH`.`id`, `PUBLISH`.`name` FROM `BOOK` INNER JOIN `PUBLISH` ON (`BOOK`.`publish_id` = `PUBLISH`.`id`);

这里我们可以看到,Django使用了INNER JOIN来获得出版社的信息。这条SQL查询得到的结果如下:

id book_name publish_id id name
1 钢铁是怎样炼成的 1 1 人民日报出版社
2 西游记 1 1 人民日报出版社
3 巴黎圣母院 2 2 北京大学出版社
4 水浒传 2 2 北京大学出版社
使用方法

函数支持如下三种用法:
*fields 参数
select_related() 接受可变长参数,每个参数是需要获取的外键(父表的内容)的字段名,以及外键的外键的字段名、外键的外键的外键…。若要选择外键的外键需要使用两个下划线“__”来连接。

例如我们要获得张三的最喜爱的书的出版社,可以用如下方式:

zhangs = Person.objects.select_related('favorite_book__publish').get(first_name="张", last_name="三")
zhangs.favorite_book.publish

触发的SQL查询如下:

SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id`, `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id`, `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PERSON` INNER JOIN `BOOK` ON (`PERSON`.`favorite_book_id` = `BOOK`.`id`) INNER JOIN `PUBLISH` ON (`BOOK`.`publish_id` = `PUBLISH`.`id`) WHERE (`PERSON`.`first_name` = '张' AND `PERSON`.`last_name` = '三');

可以看到,Django使用了2次 INNER JOIN 来完成请求,获得了BOOK表和PUBLISH表的内容并添加到结果表的相应列,这样在调用 zhangs.favorite_book的时候也不必再次进行SQL查询。

id first_name last_name favorite_book_id id book_name publish_id id name
1 1 1 钢铁是怎样炼成的 1 1 人民日报出版社

注:未指定的外键不会被添加到结果中,如果不指定外键,就会进行两次查询。如果深度更深,查询的次数更多。

Django1.7版本以上支持链式调用
Person.objects.select_related('其中一个字段').select_related('另外一个字段').get(firstname=u"张",lastname=u"三")
这样会将该表中两个外键同时查询出来

depth 参数
select_related() 接受depth参数,depth参数可以确定select_related的深度。Django会递归遍历指定深度内的所有的OneToOneField和ForeignKey。以本例说明:

zhangs = Person.objects.select_related(depth = d)
d=1 相当于 select_related(‘favorite_book’)
d=2 相当于 select_related(‘favorite_book__publish’)

无参数
select_related() 也可以不加参数,这样表示要求Django尽可能深的select_related。例如:zhangs = Person.objects.select_related().get(first_name=”张”,last_name=”三”)。但要注意两点:

  1. Django本身内置一个上限,对于特别复杂的表关系,Django可能在你不知道的某处跳出递归,从而与你想的做法不一样.
  2. 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失效,只保留最后一个

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

publish = Publish.objects.prefetch_related("book_set").get(name="北京大学出版社")
for book in publish.book_set.all():print(book.book_name)

触发的sql查询:

SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`name` = '北京大学出版社';
SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` WHERE `BOOK`.`publish_id` IN (2);

prefetch使用的是 IN 语句实现的。这样,在QuerySet中的对象数量过多的时候,根据数据库特性的不同有可能造成性能问题。
使用方法
和select_related()一样,prefetch_related()也支持深度查询,例如要获得所有first_name为张的人拥有的书的出版社:

SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON` WHERE `PERSON`.`first_name` = '张';
SELECT (`PERSON_book`.`person_id`) AS `_prefetch_related_val_person_id`, `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` INNER JOIN `PERSON_book` ON (`BOOK`.`id` = `PERSON_book`.`book_id`) WHERE `PERSON_book`.`person_id` IN (1);
SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`id` IN (1, 2);

注意:在使用QuerySet的时候,一旦在链式操作中改变了数据库请求,之前用prefetch_related缓存的数据将会被忽略掉。这会导致Django重新请求数据库来获得相应的数据,从而造成性能问题。这里提到的改变数据库请求指各种filter()、exclude()等等最终会改变SQL代码的操作。而all()并不会改变最终的数据库请求,因此是不会导致重新请求数据库的。
我们的书籍加入一本巴黎时尚杂志
举个例子,要获取所有人拥有的书中带有“巴黎”词组的书籍,这样做会导致大量的SQL查询:

peoples = Person.objects.prefetch_related('book')
print([people.book.filter(book_name__icontains="巴黎")for people in peoples])

数据库中有两条包含了"巴黎"词组,所以执行2+2次sql查询

SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON`; SELECT (`PERSON_book`.`person_id`) AS `_prefetch_related_val_person_id`, `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` INNER JOIN `PERSON_book` ON (`BOOK`.`id` = `PERSON_book`.`book_id`) WHERE `PERSON_book`.`person_id` IN (1, 2); SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` INNER JOIN `PERSON_book` ON (`BOOK`.`id` = `PERSON_book`.`book_id`) WHERE (`PERSON_book`.`person_id` = 1 AND `BOOK`.`book_name` LIKE '%巴黎%') LIMIT 21; SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` INNER JOIN `PERSON_book` ON (`BOOK`.`id` = `PERSON_book`.`book_id`) WHERE (`PERSON_book`.`person_id` = 2 AND `BOOK`.`book_name` LIKE '%巴黎%') LIMIT 21;

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

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

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

小结

  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。

使用哪个函数
如果我们想要获得最喜爱书籍的出版社为北京大学出版社的人,我们一般是先获得北京大学出版社,再获得北京大学出版社的所有书籍,最后获得最喜爱书籍的人。就像这样:

    publish = Publish.objects.get(name="北京大学出版社")people = []for book in publish.book_set.all():people.extend(book.favorite_book.all())

这样做会导致1+(书籍数目)次SQL查询,

SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`name` = '北京大学出版社';
SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` WHERE `BOOK`.`publish_id` = 2;
SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON` WHERE `PERSON`.`favorite_book_id` = 3;
SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON` WHERE `PERSON`.`favorite_book_id` = 4;
SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON` WHERE `PERSON`.`favorite_book_id` = 5;

使用prefetch_related()进行查询

publish = Publish.objects.prefetch_related("book_set__favorite_book").get(name="北京大学出版社")
people = []
for book in publish.book_set.all():people.extend(book.favorite_book.all())
print(people)

因为是一个深度为2的prefetch,所以会导致3次SQL查询:

SELECT `PUBLISH`.`id`, `PUBLISH`.`name` FROM `PUBLISH` WHERE `PUBLISH`.`name` = '北京大学出版社';
SELECT `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id` FROM `BOOK` WHERE `BOOK`.`publish_id` IN (2);
SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id` FROM `PERSON` WHERE `PERSON`.`favorite_book_id` IN (3, 4, 5);

倒过来查询会更简单:
我们平时写的时候会这样:

peoples = Person.objects.filter(favorite_book__publish__name="北京大学出版社")
print(peoples)

sql语句这样

SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id`
FROM `PERSON`
INNER JOIN `BOOK` ON (`PERSON`.`favorite_book_id` = `BOOK`.`id`)
INNER JOIN `PUBLISH` ON (`BOOK`.`publish_id` = `PUBLISH`.`id`)
WHERE `PUBLISH`.`name` = '北京大学出版社' LIMIT 21;

使用
peoples =

Person.objects.select_related("favorite_book__publish").filter(favorite_book__publish__name="北京大学出版社")
print(peoples)

sql语句这样

SELECT `PERSON`.`id`, `PERSON`.`first_name`, `PERSON`.`last_name`, `PERSON`.`favorite_book_id`, `BOOK`.`id`, `BOOK`.`book_name`, `BOOK`.`publish_id`, `PUBLISH`.`id`, `PUBLISH`.`name`
FROM `PERSON`
INNER JOIN `BOOK` ON (`PERSON`.`favorite_book_id` = `BOOK`.`id`)
INNER JOIN `PUBLISH` ON (`BOOK`.`publish_id` = `PUBLISH`.`id`)
WHERE `PUBLISH`.`name` = '北京大学出版社' LIMIT 21;

SQL查询的数量减少了,python程序上也精简了。
select_related()的效率要高于prefetch_related()。因此,最好在能用select_related()的地方尽量使用它,也就是说,对于ForeignKey字段,避免使用prefetch_related()。

总结:

  1. 因为select_related()总是在单次SQL查询中解决问题,而prefetch_related()会对每个相关表进行SQL查询,因此select_related()的效率通常比后者高。
  2. 鉴于第一条,尽可能的用select_related()解决问题。只有在select_related()不能解决问题的时候再去想prefetch_related()。
  3. 你可以在一个QuerySet中同时使用select_related()和prefetch_related(),从而减少SQL查询的次数。
  4. 只有prefetch_related()之前的select_related()是有效的,之后的将会被无视掉。

Django优化(减少数据库查询次数)---select_related和prefetch_related的使用相关推荐

  1. php查询数据库如何降低负荷,WordPress通过减少数据库查询次数来优化性能的方法...

    说起WordPress优化来算是一个老生常谈的话题了,WordPress很慢,这是很多人都在说的,我记得此论调也就是这几年才流行开的,据说是因为一个台湾的WordPress大咖在自己博客上宣称&quo ...

  2. 优化Oracle数据库查询10个方法

    优化Oracle数据库查询10个方法: 网页制作中数据的查询是最主要的功能之一,其性能的好坏直接关系到网页的的运行效率.下面对Oracle数据库中大家不太注意的查询方法做一些总结. 第一个方法:利用连 ...

  3. 网站优化-减少DNS查询

    什么是DNS? DNS(Domain Name System)译为 域名系统,它是 Internet 的一项核心服务,可以将域名和IP地址相互映射的一个 分布式数据库 ,能够让你方便的访问互联网.   ...

  4. 计算一个页面中的数据库查询次数和用时

    很多博客软件都有这么一个功能,比如"生成本次页面一共花费了xx毫秒,进行了xx次数据库查询"等等.那么这个功能是如何实现的呢,下面我大概说下思路. 1. 在类的构造函数中声明全局变 ...

  5. GG有这个提醒怎么办 合并外部 JavaScript 尽量减少 DNS 查询次数 由同一网址提供资源...

    合并外部 JavaScript 尽量减少 DNS 查询次数 由同一网址提供资源 合并外部 JavaScript www.xiangdang.net提供了 5 个 JavaScript 文件,应该将其合 ...

  6. wordpress配置Memcached缓存优化数据库查询次数

    今天看了一下很多人的网站,首页查询次数200次还能打开,就速度慢了一点,这里水篇文章,使用Memcached后减少三分之二. 首先在宝塔Linux面板安装Memcached扩展,在你网站使用中的PHP ...

  7. JS性能优化——减少DOM操作次数

    DOM操作会导致浏览器重解析,引起重排(回流)和重绘,直接影响页面性能 对DOM的操作一般有两种:修改已存在页面上的DOM元素(更改样式),创建插入新的DOM元素 这里提出几个方案来减少DOM操作次数 ...

  8. mysql 性能优化,减轻数据库的压力。(减少数据库查询的次数)

    查询订单相应的user_id的详细 $arr = array(1=>array('user_id' => 5,'good_id' => '361'),2=>array('use ...

  9. 三方法优化MySQL数据库查询

    任何一位数据库程序员都会有这样的体会:高通信量的数据库驱动程序中,一条糟糕的SQL查询语句可对整个应用程序的运行产生严重的影响,其不仅消耗掉更多的数据库时间,且它将对其他应用组件产生影响. 如同其它学 ...

  10. mysql 数据查询优化_优化MySQL数据库查询的三种方法

    任何一位数据库程序员都会有这样的体会:高通信量的数据库驱动程序中,一条糟糕的SQL查询语句可对整个应用程序的运行产生严重的影响,其不仅消耗掉更多的数据库时间,且它将对其他应用组件产生影响. 如同其它学 ...

最新文章

  1. Linux的磁盘满了
  2. 回归分析结果表格怎么填_手把手教绘制回归分析结果的森林图GraphPad Prism和Excel...
  3. 受教黑金文档,再度优化兼容irq uart代码
  4. oracle数据库读取工具,用Oracle导入导出工具实现Oracle数据库移植
  5. BZOJ3453 XLkxc(拉格朗日插值)
  6. mybatis和spring整合版本对应表
  7. 分模块的maven项目调试时报Source not found的解决办法
  8. 2 str转byte失败_android 4.2的多线程库加载崩溃问题
  9. redis与数据库同步的解决方案
  10. paypal java_PaypalUtil PayPal付款JAVA工具类
  11. 抖音运营详细教程,算法解读、平台规则、热门涨粉......丨国仁网络
  12. 5G技术对我们生活的影响
  13. 温度能够瞬间提升到千度以上?究竟是什么原理
  14. EasyExcel导出添加水印
  15. DP4344音频转换芯片ACD/DAC完全兼容CS4344音频解码
  16. Linux 进行yum 安装是出现文件 conflicts 解决方法
  17. 微信公众号获取AccessToken
  18. 【Android 插件化】VirtualApp 编译运行 ( VirtualApp 简介 | 配置 VirtualApp 编译环境 | 编译运行 VirtualApp 官方示例 )
  19. 计算机专业论文题目_kaic
  20. FlexRay™ 协议控制器 (E-Ray)-06

热门文章

  1. LinearLayout和RelativeLayout布局中使用android:orientation
  2. 列宽一字符等于多少厘米_【excle列宽等于】excel里面的列宽和行高单位是多少?多少等于1厘米?怎么对比的?...
  3. (一)RFB协议概述
  4. 《联盟》读书笔记(二)
  5. 抽象代数的代码实现(6)自同构、正规子群、商群
  6. cpu和显卡瓶颈测试软件,CPU与GPU瓶颈的详述
  7. shopex使用经验
  8. 填坑Ⅰ | 简单的数据结构
  9. 150行Python代码模拟太阳系行星运转(含music)
  10. css边框图片的使用场景