• pythonのdjango select_related 和 prefetch_related()


    在数据库有外键的时候,使用select_related() 和 prefetch_related() 可以很好的减少数据库请求次数,从而提高性能。

    (1)select_related()当执行它的查询时它沿着外键关系查询关联的对象数据。它会生成一个复杂的查询并引起性能的消耗,但是在以后使用外键关系时将不需要数据库查询。
    (2)prefetch_related()返回的也是QuerySet,它将在单个批处理中自动检索每个指定查找的对象。这具有与select_related类似的目的,两者都被设计为阻止由访问相关对象而导致的数据库查询的泛滥,但是策略是完全不同的。
    (3)select_related通过创建SQL连接并在SELECT语句中包括相关对象的字段来工作。因此,select_related在同一数据库查询中获取相关对象。然而,为了避免由于跨越“多个'关系而导致的大得多的结果集,select_related限于单值关系 -外键和一对一关系。
    (4)prefetch_related,另一方面,为每个关系单独查找,并在Python中“加入”。这允许它预取多对多和多对一对象,除了外键和一对一关系,它们不能使用select_related来完成。

    依据图示,编写model代码,model.py代码如下:

     1 from django.db import models
     2 
     3 # Create your models here.
     4 
     5 class Province(models.Model):
     6     name = models.CharField(max_length=10)
     7     def __str__(self):
     8         return self.name
     9 
    10 class City(models.Model):
    11     name = models.CharField(max_length=5)
    12     province = models.ForeignKey(Province,on_delete=True,null=True)
    13     def __str__(self):
    14         return  self.name
    15 
    16 class Person(models.Model):
    17     firstname = models.CharField(max_length=10)
    18     lastname = models.CharField(max_length=10)
    19     visitation = models.ManyToManyField(City,related_name="visitor") # visitation字段与city表时多对多关系
    20     hometown = models.ForeignKey(City,related_name="birth",on_delete=True,null=True) # related_name 是直接给外键起好名字
    21     living = models.ForeignKey(City,related_name="citizen",on_delete=True,null=True)
    22     def __str__(self):
    23         return self.firstname + self.lastname

    app名称为stark,我们在province中添加如下数据:

    INSERT INTO `modeltest`.`stark_province` (`id`, `name`) VALUES ('1', '北京');
    INSERT INTO `modeltest`.`stark_province` (`id`, `name`) VALUES ('2', '河南');

    在city中添加如下数据:

    INSERT INTO `modeltest`.`stark_city` (`id`, `name`, `province_id`) VALUES ('1', '昌平', '1');
    INSERT INTO `modeltest`.`stark_city` (`id`, `name`, `province_id`) VALUES ('2', '海淀', '1');
    INSERT INTO `modeltest`.`stark_city` (`id`, `name`, `province_id`) VALUES ('3', '郑州', '2');
    INSERT INTO `modeltest`.`stark_city` (`id`, `name`, `province_id`) VALUES ('4', '焦作', '2');

    select_related()

    文章解释:https://www.cnblogs.com/tuifeideyouran/p/4232028.html

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

    在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。

    But我实际应用时:

      并没有发现什么好处

    不用select_related()情况的一个例子:

     obj1 = models.City.objects.all()
        for c in obj1:
            print(c.province)

    这样会导致线性的SQL查询,SQL查询语句如下:

    (0.001) SELECT @@SQL_AUTO_IS_NULL; args=None
    (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
    (0.000) SELECT `stark_city`.`id`, `stark_city`.`name`, `stark_city`.`province_id` FROM `stark_city`; args=()
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 1; args=(1,)
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 1; args=(1,)
    (0.000) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 2; args=(2,)

    用select_related()的例子:

    obj2 = models.City.objects.select_related().all()
        for c1 in obj2:
            print(c1.province)

    控制台打印结果如下:

    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 2; args=(2,)
    (0.001) SELECT `stark_city`.`id`, `stark_city`.`name`, `stark_city`.`province_id` FROM `stark_city`; args=()
    (0.000) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 1; args=(1,)
    (0.000) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 1; args=(1,)
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 2; args=(2,)
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` = 2; args=(2,)
    北京
    北京
    河南
    河南

    经过对比SQL,几乎呈现一直状态,并未实现我参考的文章中所出现的:

    感受不到这种优越感~~~~~

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

    例如获得张三的现居省份

     print("------------------------")
        zhangs = models.Person.objects.select_related("hometown__province","living__province").get(firstname="",lastname="")
        print(zhangs)

    控制台数据SQL如下:

    (0.000) SELECT @@SQL_AUTO_IS_NULL; args=None
    (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
    (0.003) SELECT `stark_person`.`id`, `stark_person`.`firstname`, `stark_person`.`lastname`, 
    `stark_person`.`hometown_id`, `stark_person`.`living_id`, `stark_city`.`id`, `stark_city`.`name`,
    `stark_city`.`province_id`, `stark_province`.`id`, `stark_province`.`name`, T4.`id`, T4.`name`,
    T4.`province_id`, T5.`id`, T5.`name` FROM `stark_person` LEFT OUTER JOIN `stark_city` ON
    (`stark_person`.`hometown_id` = `stark_city`.`id`) LEFT OUTER JOIN `stark_province` ON
    (`stark_city`.`province_id` = `stark_province`.`id`) LEFT OUTER JOIN `stark_city` T4 ON
    (`stark_person`.`living_id` = T4.`id`) LEFT OUTER JOIN `stark_province` T5 ON
    (T4.`province_id` = T5.`id`) WHERE (`stark_person`.`firstname` = '' AND `stark_person`.`lastname` = ''); args=('', '') [08/Jan/2019 14:36:03] "GET /index/ HTTP/1.1" 200 2 张三

    另外,django1.7以后两个外键同时关联使用时可以采用以下方法:

        print("------------------------")
        zhangs = models.Person.objects.select_related("living__province").select_related("hometown__province").get(firstname="",lastname="")
        print(zhangs)
        return HttpResponse("ok")

    django使用了三次LEFT OUTER JOIN来完成请求。而之前这边使用的INNER JOIN 来完成请求,两种关联查询区别在于:

    -------------------------------------------------------------------额外知识点SQL-------------------------------------------------------------------

    Table A 是左边的表。Table B 是右边的表。

    1.INNER JOIN 产生的结果是AB的交集

      SELECT * FROM TableA INNER JOIN TableB ON TableA.name = TableB.name

      INNER JOIN 产生的结果是AB的交集          INNER JOIN 产生的结果是AB的交集

    2.LEFT [OUTER] JOIN 产生表A的完全集,而B表中匹配的则有值,没有匹配的则以null值取代。

      SELECT * FROM TableA LEFT OUTER JOIN TableB ON TableA.name = TableB.name

      LEFT [OUTER] JOIN 产生表A的完全集,而B表中匹配的则有值           LEFT [OUTER] JOIN 产生表A的完全集,而B表中匹配的则有值

    3.RIGHT [OUTER] JOIN 产生表B的完全集,而A表中匹配的则有值,没有匹配的则以null值取代。

      SELECT * FROM TableA RIGHT OUTER JOIN TableB ON TableA.name = TableB.name

      与left join类似。

    4.FULL [OUTER] JOIN 产生A和B的并集。对于没有匹配的记录,则会以null做为值。

      SELECT * FROM TableA FULL OUTER JOIN TableB ON TableA.name = TableB.name 

      你可以通过is NULL将没有匹配的值找出来:

      SELECT * FROM TableA FULL OUTER JOIN TableB ON TableA.name = TableB.name

      WHERE TableA.id IS null OR TableB.id IS null 

       FULL [OUTER] JOIN 产生A和B的并集           FULL [OUTER] JOIN 产生A和B的并集

    5. CROSS JOIN 把表A和表B的数据进行一个N*M的组合,即笛卡尔积。如本例会产生4*4=16条记录,在开发过程中我们肯定是要过滤数据,所以这种很少用。

      SELECT * FROM TableA CROSS JOIN TableB 

    相信大家对inner join、outer join和cross join的区别一目了然了。

    -------------------------------------------------------------------end-------------------------------------------------------------------

    由于我不知道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失效,只保留最后一个。

    prefetch_related()

    对于多对多字段(ManyToManyField)和一对多(ForeignKey)字段,可以使用prefetch_related()来进行优化.

    prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者 是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL 语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。

    prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。

    如果我们要获得张三所有去过的城市.

    zhangs = models.Person.objects.prefetch_related("visitation").get(firstname="",lastname="")
        for city in zhangs.visitation.all():
            print(city)

    控制台输出如下:

    ------------------------
    (0.001) SELECT @@SQL_AUTO_IS_NULL; args=None
    (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
    (0.002) SELECT `stark_person`.`id`, `stark_person`.`firstname`, `stark_person`.`lastname`, `stark_person`.`hometown_id`,
    `stark_person`.`living_id`
    FROM `stark_person`
    WHERE (`stark_person`.`firstname` = '' AND `stark_person`.`lastname` = ''); args=('', '')
    昌平 海淀 郑州
    (
    0.001) SELECT (`stark_person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `stark_city`.`id`,
    `stark_city`.`name`, `stark_city`.`province_id`
    FROM `stark_city`
    INNER JOIN `stark_person_visitation` ON (`stark_city`.`id` = `stark_person_visitation`.`city_id`)
    WHERE `stark_person_visitation`.`person_id` IN (1); args=(1,) [08/Jan/2019 15:55:03] "GET /index/ HTTP/1.1" 200 2

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

    或者我们要获得河南的所有城市名

    hn = models.Province.objects.prefetch_related("city_set").get(name__iexact='河南')
        for city in hn.city_set.all():
            print(city.name)

    控制台输出SQL语句如下:

    ------------------------
    (0.000) SELECT @@SQL_AUTO_IS_NULL; args=None
    (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`name` LIKE '河南'; args=('河南',)
    郑州
    (0.001) SELECT `stark_city`.`id`, `stark_city`.`name`, `stark_city`.`province_id` 
    FROM `stark_city` WHERE `stark_city`.`province_id` IN (2); args=(2,) 焦作 [08/Jan/2019 16:04:50] "GET /index/ HTTP/1.1" 200 2

    例如要获得所有姓王的人去过的省:

    wangs = models.Person.objects.prefetch_related("visitation__province").filter(firstname__iexact='')
        for i in wangs :
            print(i)
            for city in i.visitation.all():
                print(city.province.name)

    控制台SQL代码如下:

    *************************
    (0.001) SELECT `stark_person`.`id`, `stark_person`.`firstname`, `stark_person`.`lastname`, `stark_person`.`hometown_id`, `stark_person`.`living_id` FROM `stark_person` WHERE `stark_person`.`firstname` LIKE ''; args=('',)
    (0.001) SELECT (`stark_person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `stark_city`.`id`, `stark_city`.`name`, `stark_city`.`province_id` FROM `stark_city` INNER JOIN `stark_person_visitation` ON (`stark_city`.`id` = `stark_person_visitation`.`city_id`) WHERE `stark_person_visitation`.`person_id` IN (2); args=(2,)
    (0.001) SELECT `stark_province`.`id`, `stark_province`.`name` FROM `stark_province` WHERE `stark_province`.`id` IN (1, 2); args=(1, 2)
    [08/Jan/2019 17:06:03] "GET /index/ HTTP/1.1" 200 2
    Person object (2)
    北京
    河南
    河南

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

    None

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

    prefetch_cleared_qset = zhangs.prefetch_related(None)

    select_related()的效率要高于prefetch_related()。因此,最好在能用select_related()的地方尽量使用它,也就是说,对于ForeignKey字段,避免使用prefetch_related()。

    django使用了两次INNER JOIN来完成请求,但是未指定的外键则不会被添加到结果中,例如张三的故乡.

  • 相关阅读:
    ORA-01033: ORACLE initialization or shutdown in progress
    String.Format 格式化货币的小问题
    SQL Server——存储过程
    如何书写优雅、漂亮的SQL脚本?
    清除Chrome浏览器的历史记录、缓存
    SQL模板资源管理器,你用了吗?
    WPF笔记(2.4 Grid)
    python实现插入排序
    冒泡算法实现
    python获取本地ip地址的方法
  • 原文地址:https://www.cnblogs.com/pengpengzhang/p/10240259.html
Copyright © 2020-2023  润新知