准备

定义如下模型

 from django.db import models

 # 省份
class Province(models.Model):
name = models.CharField(max_length=10) # 城市
class City(models.Model):
name = models.CharField(max_length=5)
province = models.ForeignKey(Province) # 人
class Person(models.Model):
name = models.CharField(max_length=20)
# 去过的城市
visitation = models.ManyToManyField(City, related_name="visitor")
# 故乡
hometown = models.ForeignKey(City, related_name="birth")
# 居住地
living = models.ForeignKey(City, related_name="citizen")

Code

province表:

city表:

person表:

初始数据如下:

示例模型是用来记录各个人的故乡、居住地、以及到过的城市。

select_related

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

示例

  • 查询城市所属省份

    • 未优化
           city_list = models.City.objects.all()
      [print('{}=>{}'.format(city.name, city.province.name)) for city in city_list] '''
      result:
      武汉市=>湖北省
      孝感市=>湖北省
      广州市=>广东省
      深圳市=>广东省
      '''

      Code

       (0.000) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city`; args=()
      (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,)
      (0.001) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,)
      (0.001) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 2; args=(2,)
      (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 2; args=(2,)

      执行sql:

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

    • 已优化
           city_list = models.City.objects.select_related().all()
      [print('{}=>{}'.format(city.name, city.province.name)) for city in city_list] '''
      result:
      武汉市=>湖北省
      孝感市=>湖北省
      广州市=>广东省
      深圳市=>广东省
      '''

      Code

       (0.001) SELECT `city`.`id`, `city`.`name`, `city`.`province_id`, `province`.`id`, `province`.`name` FROM `city` INNER JOIN `province` ON (`city`.`province_id` = `province`.`id`); args=()

      执行sql:

      在对QuerySet使用select_related()函数后,Django会一次性获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。这里我们可以看到,Django使用了INNER JOIN来获得省份的信息。

参数说明

  • *filed

    select_related() 接受可变长参数,每个参数是需要获取的外键(父表的内容)的字段名,以及外键的外键的字段名、外键的外键的外键...。若要选择外键的外键需要使用两个下划线“__”来连接。例如我们要获得张三的现居省份,可以用如下方式:

         p = models.Person.objects.select_related('living__province').get(name='张三')
    print(p.living.province.name) '''
    result:
    广东省
    '''

    Code

     (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id`, `city`.`id`, `city`.`name`, `city`.`province_id`, `province`.`id`, `province`.`name` FROM `person` INNER JOIN `city` ON (`person`.`living_id` = `city`.`id`) INNER JOIN `province` ON (`city`.`province_id` = `province`.`id`) WHERE `person`.`name` = '张三'; args=('张三',)

    执行sql:

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

    然而,未指定的外键则不会被添加到结果中。这时候如果需要获取张三的故乡就会进行SQL查询了:

         p = models.Person.objects.select_related('living__province').get(name='张三')
    print(p.hometown.province.name) '''
    result:
    湖北省
    '''

    Code

     (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id`, `city`.`id`, `city`.`name`, `city`.`province_id`, `province`.`id`, `province`.`name` FROM `person` INNER JOIN `city` ON (`person`.`living_id` = `city`.`id`) INNER JOIN `province` ON (`city`.`province_id` = `province`.`id`) WHERE `person`.`name` = '张三'; args=('张三',)
    (0.000) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` WHERE `city`.`id` = 2; args=(2,)
    (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,)

    执行sql:

    如果不指定外键,就会进行两次查询。如果深度更深,查询的次数更多。

  • 无参

    select_related()也可以不加参数,这样表示要求Django尽可能深的select_related。例如:

         p = models.Person.objects.select_related().get(name='张三')
    print(p.hometown.province.name)
    print(p.living.province.name) '''
    result:
    湖北省
    广东省
    '''

    Code

     (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id`, `city`.`id`, `city`.`name`, `city`.`province_id`, `province`.`id`, `province`.`name`, T4.`id`, T4.`name`, T4.`province_id`, T5.`id`, T5.`name` FROM `person` INNER JOIN `city` ON (`person`.`hometown_id` = `city`.`id`) INNER JOIN `province` ON (`city`.`province_id` = `province`.`id`) INNER JOIN `city` T4 ON (`person`.`living_id` = T4.`id`) INNER JOIN `province` T5 ON (T4.`province_id` = T5.`id`) WHERE `person`.`name` = '张三'; args=('张三',)

    执行sql

    注意:

    • Django本身内置一个上限,对于特别复杂的表关系,Django可能在你不知道的某处跳出递归,从而与你想的做法不一样。

    • Django并不知道你实际要用的字段有哪些,所以会把所有的字段都抓进来,从而会造成不必要的浪费而影响性能。

小结

  • select_related主要针一对一和多对一关系进行优化。
  • select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  • 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,如果要访问的话Django会再次进行SQL查询。
  • 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。

prefetch_related

对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。或许你会说,没有一个叫OneToManyField的东西啊。实际上,使用ForeignKey的字段就是一个多对一的字段,而被ForeignKey关联的字段就是一对多字段了。prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。

示例

  • 获得张三所有去过的城市

     p = models.Person.objects.prefetch_related('visitation').get(name='张三')
    [print(c.name) for c in p.visitation.all()]
    '''
    result:
    武汉市
    孝感市
    广州市
    深圳市
    '''

    Code

     (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person` WHERE `person`.`name` = '张三'; args=('张三',)
    (0.000) SELECT (`person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` INNER JOIN `person_visitation` ON (`city`.`id` = `person_visitation`.`city_id`) WHERE `person_visitation`.`person_id` IN (1); args=(1,)

    执行sql:

  • 获得湖北的所有城市名

     hb = models.Province.objects.prefetch_related('city_set').get(name__iexact=u"湖北省")
    for city in hb.city_set.all():
    print(city.name)
    '''
    result:
    武汉市
    孝感市
    '''

    Code

     (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`name` LIKE '湖北省'; args=('湖北省',)
    (0.001) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` WHERE `city`.`province_id` IN (1); args=(1,)

    执行sql:

参数说明

  • *lookups

    和select_related()一样,prefetch_related()也支持深度查询,例如要获得所有姓张的人去过的省:

     p_list = models.Person.objects.filter(name__iexact='张三').prefetch_related('visitation__province').all()
    for i in p_list:
    for city in i.visitation.all():
    print(city.province.name)
    '''
    result:
    湖北省
    湖北省
    广东省
    广东省
    '''

    Code

     (0.001) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person` WHERE `person`.`name` LIKE '张三'; args=('张三',)
    (0.000) SELECT (`person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` INNER JOIN `person_visitation` ON (`city`.`id` = `person_visitation`.`city_id`) WHERE `person_visitation`.`person_id` IN (1); args=(1,)
    (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` IN (1, 2); args=(1, 2)

    执行sql:

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

     plist = models.Person.objects.prefetch_related('visitation')
    l = [p.visitation.filter(name__icontains=u"市") for p in plist]
    for i in l:
    for j in i:
    print(j.name)
    '''
    result:
    武汉市
    孝感市
    广州市
    深圳市
    '''

    Code

     (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person`; args=()
    (0.001) SELECT (`person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` INNER JOIN `person_visitation` ON (`city`.`id` = `person_visitation`.`city_id`) WHERE `person_visitation`.`person_id` IN (1); args=(1,)
    (0.000) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` INNER JOIN `person_visitation` ON (`city`.`id` = `person_visitation`.`city_id`) WHERE (`person_visitation`.`person_id` = 1 AND `city`.`name` LIKE '%市%'); args=(1, '%市%')

    执行sql:

    因为数据库中有1人,导致了2+1次SQL查询。详细分析一下这些请求事件。众所周知,QuerySet是lazy的,要用的时候才会去访问数据库。运行到第二行Python代码时,for循环将plist看做iterator,这会触发数据库查询。最初的两次SQL查询就是prefetch_related导致的。虽然已经查询结果中包含所有所需的city的信息,但因为在循环体中对Person.visitation进行了filter操作,这显然改变了数据库请求。因此这些操作会忽略掉之前缓存到的数据,重新进行SQL查询。但是如果有这样的需求了应该怎么办呢?可以在Python中完成这部分操作:

     plist = models.Person.objects.prefetch_related('visitation')
    [[print(city.name) for city in p.visitation.all() if u"市" in city.name] for p in plist]
    '''
    result:
    武汉市
    孝感市
    广州市
    深圳市
    '''

    Code

     (0.001) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person`; args=()
    (0.001) SELECT (`person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` INNER JOIN `person_visitation` ON (`city`.`id` = `person_visitation`.`city_id`) WHERE `person_visitation`.`person_id` IN (1); args=(1,)

    执行sql:

小结

  • prefetch_related主要针一对多和多对多关系进行优化。
  • prefetch_related通过分别获取各个表的内容,然后用Python处理他们之间的关系来进行优化。
  • 可以通过可变长参数指定需要prefetch_related的字段名。指定方式和特征与select_related是相同的。

python框架之Django(6)-查询优化之select_related&prefetch_related的更多相关文章

  1. python框架之django

    python框架之django 本节内容 web框架 mvc和mtv模式 django流程和命令 django URL django views django temple django models ...

  2. 第六篇:web之python框架之django

    python框架之django   python框架之django 本节内容 web框架 mvc和mtv模式 django流程和命令 django URL django views django te ...

  3. Python框架之Django学习

    当前标签: Django   Python框架之Django学习笔记(十四) 尛鱼 2014-10-12 13:55 阅读:173 评论:0     Python框架之Django学习笔记(十三) 尛 ...

  4. Python框架之Django的相册组件

    Python框架之Django的相册组件 恩,没错,又是Django,虽然学习笔记已经结贴,但是学习笔记里都是基础的,Django的东西不管怎么说还是很多的,要学习的东西自然不会仅仅用十几篇博文就能学 ...

  5. Python框架之Django学习笔记(十一)

    话说上次说到数据库的基本访问,而数据库我们主要进行的操作就是CRUD,也即是做计算处理时的增加(Create).读取(Retrieve)(重新得到数据).更新(Update)和删除(Delete),俗 ...

  6. python框架之Django(13)-admin组件

    使用 Django 提供了基于 web 的管理工具. Django 自动管理工具是 django.contrib 的一部分.你可以在项目的 settings.py 中的 INSTALLED_APPS ...

  7. Python框架之Django学习笔记(十二)

    Django站点管理 十一转眼结束,说好的充电没能顺利开展,反而悠闲的看了电视剧以及去影院看了新上映的<心花路放>.<亲爱的>以及<黄金时代>,说好的劳逸结合现在回 ...

  8. Python框架之Django学习笔记(十)

    又是一周周末,如约学习Django框架.在上一次,介绍了MVC开发模式以及Django自己的MVT开发模式,此次,就从数据处理层Model谈起. 数据库配置 首先,我们需要做些初始配置:我们需要告诉D ...

  9. Python框架之Django学习笔记(九)

    模型 之前,我们用 Django 建造网站的基本途径: 建立视图和 URLConf . 正如我们所阐述的,视图负责处理一些主观逻辑,然后返回响应结果. 作为例子之一,我们的主观逻辑是要计算当前的日期和 ...

随机推荐

  1. Python Every Class Needs a __repr__

    一.思考 当我们在Python中定义一个类的时候,如果我们通过print打印这个类的实例化对象,或者我们直接输入这个类实例化对象会返回怎么样的结果,如下代码: >>> class P ...

  2. Atitit 3种类型的公司:运营驱动型;产品驱动型; 技术驱动型。

    Atitit  3种类型的公司:运营驱动型:产品驱动型: 技术驱动型. 领导驱动,产品驱动,运营驱动还是工程师驱动 3种类型的公司: 一种是运营驱动型: 一种是产品驱动型: 一种技术驱动型. 运营驱动 ...

  3. vue2.0 项目搭建 和vue 2.0 electron 项目搭建

    1.关于electron vue 项目的搭建 全局或者局部安装项目vue: 脚手架指令生成: npm install -g vue-cli vue init simulatedgreg/electro ...

  4. 代理_正向代理_反向代理_nginx_转

    转自:Nginx 相关介绍(Nginx是什么?能干嘛?)   蔷薇Nina 关于代理 说到代理,首先我们要明确一个概念,所谓代理就是一个代表.一个渠道: 此时就设计到两个角色,一个是被代理角色,一个是 ...

  5. RTX任务管理

        默认情况下用户创建的任务栈大小是由参数Task stack size决定的.     如果觉得每个任务都分配同样大小的栈空间不方便的话,可以采用自定义任务栈的方式创建任务.采用自定义方式更灵活 ...

  6. numpy学习之创建数组

    1.使用array函数创建数组 import numpy as np ndarray1 = np.array([1, 2, 3]) array([1, 2, 3]) ndarray2 = np.arr ...

  7. thinphp5框架遇到 mkdir() Permission denied 解决办法

    网站重装 直接复制本地程序文件 里面数据库链接信息要改成线上的 然后mysql apache 等都没有动 运行后出现错误 mkdir() Permission denied 这是由于runtime目录 ...

  8. 【转载】C++中替代sprintf的std::ostringstream输出流详解

    一.简单介绍 ostringstream是C++的一个字符集操作模板类,定义在sstream.h头文件中.ostringstream类通常用于执行C风格的串流的输出操作,格式化字符串,避免申请大量的缓 ...

  9. [原]shell批量文件增删改前后缀

    改后缀 for files in `ls *`; do mv ${files} ${files}.bak; done 改后缀 rename .bak .bak2 * 去除后缀 for files in ...

  10. exception ‘PHPExcel_Calculation_Exception‘ with message ‘粉丝数据!C2679 -> Formula Error: Operator ‘=‘ has no operands

    导致问题原因可能是导出字段中包含有  ‘=’  ,解决办法:在字段前拼上一个半单引号. if(strpos($lists[$i-2][‘nickname‘],‘=‘) === 0){ $lists[$ ...