day71学习笔记之ORM分组补充、select_related与prefetch_related函数

1  ORM分组补充

1.1  创建表格

首先在models.py中创建表格类:

from django.db import models

# Create your models here.


class Employee(models.Model):
    name = models.CharField(max_length=16)
    age = models.IntegerField()
    salary = models.IntegerField()
    province = models.CharField(max_length=32)
    dept = models.CharField(max_length=16)

    def __str__(self):
        return self.name

    class Meta:
        db_table = "employee" #指定表名为employee,而不是app01_employee


class Employee2(models.Model):
    name = models.CharField(max_length=16)
    age = models.IntegerField()
    salary = models.IntegerField()
    province = models.CharField(max_length=32)
    dept = models.ForeignKey(to="Dept")

    def __str__(self):
        return self.name

    class Meta:
        db_table = "employee2"


class Dept(models.Model):
    name = models.CharField(max_length=16, unique=True)

    def __str__(self):
        return self.name


    class Meta:
        db_table = "dept2"



class Author(models.Model):
    name = models.CharField(max_length=32)
    books = models.ManyToManyField(to="Book")

    def __str__(self):
        return self.name

    class Meta:
        db_table = "author"


class Book(models.Model):
    title = models.CharField(max_length=32)

    def __str__(self):
        return self.title

    class Meta:
        db_table = "book"
View Code

表格中数据如下:

  1.employee

 2.employee2

 3.dept2

1.2  分组测试

引入Django环境:

import os
import sys


if __name__ == '__main__':
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ormday71.settings")
    import django
    django.setup()

    from app01 import models

    from django.db.models import Avg
View Code

 示例1:

ret = models.Employee.objects.all().annotate(avg=Avg("salary"))
print(ret)  #结果:<QuerySet [<Employee: 王二狗>, <Employee: 学习>, <Employee: 狼爷>, <Employee: 狼爸>, <Employee: 大卫>]>
print(ret[0].avg)  #结果:10.0

#对应的SQL语句为:SELECT `employee`.`id`, `employee`.`name`, `employee`.`age`, `employee`.`salary`, `employee`.`province`, `employee`.`dept`, AVG(`employee`.`salary`) AS `avg` FROM `employee` GROUP BY `employee`.`id` ORDER BY NULL LIMIT 21;

可以看出默认情况下是以employee.id为分组条件,ret为一个QuerySet列表,包含多个Employee类的对象

示例2:

ret = models.Employee.objects.values("dept").annotate(avg=Avg("salary"))
print(ret)  #结果:<QuerySet [{'dept': '门卫', 'avg': 30.0}, {'dept': '财务', 'avg': 100.0}, {'dept': '化学', 'avg': 1000.0}, {'dept': '机械', 'avg': 10000.0}]>

#对应的SQL语句:SELECT `employee`.`dept`, AVG(`employee`.`salary`) AS `avg` FROM `employee` GROUP BY `employee`.`dept` ORDER BY NULL LIMIT 21;

返回的结果中,默认包含两个字段信息,dept和avg,并且分组条件为.values()中的dept字段。ret为一个QuerySet列表,包含多个字典

示例3:

ret = models.Employee.objects.values("dept").annotate(avg=Avg("salary")).values("dept","avg")
print(ret)  #结果:<QuerySet [{'dept': '门卫', 'avg': 30.0}, {'dept': '财务', 'avg': 100.0}, {'dept': '化学', 'avg': 1000.0}, {'dept': '机械', 'avg': 10000.0}]>

#对应的SQL语句:SELECT `employee`.`dept`, AVG(`employee`.`salary`) AS `avg` FROM `employee` GROUP BY `employee`.`dept` ORDER BY NULL LIMIT 21;

示例3的结果和示例2的完全相同,并且SQL语句也相同。

示例4:

ret = models.Employee.objects.values("dept").annotate(avg=Avg("salary")).values("province", "avg")
print(ret)  #结果:<QuerySet [{'province': '江苏', 'avg': 30.0}, {'province': '陕西', 'avg': 100.0}, {'province': '北京', 'avg': 1000.0}, {'province': '陕西', 'avg': 10000.0}]>

#对应的SQL语句:SELECT `employee`.`province`, AVG(`employee`.`salary`) AS `avg` FROM `employee` GROUP BY `employee`.`dept`, `employee`.`province` ORDER BY NULL LIMIT 21;

分组条件为employee.deptemployee.province,但是结果中的平均值和示例2、示例3的一样。

2  QuerySet方法补充

2.1  select_related

性能相关:表之间进行join连表操作,一次性获取关联的数据。

2.1.1  测试实例1

内容来自点我

在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.lastnamefrom django.db import models
View Code

注1:创建的app名为“app01”

注2:为了简化起见,`app01_province` 表中只有2条数据:湖北省和广东省,`app01_city`表中只有三条数据:武汉市、十堰市和广州市

app01_city表:

app01_province表:

 

app01_person表:

 

app01_person_visitation表(多对多):

 

 (1)如果我们需要打印数据库中的所有市及其所属省份,最直接的做法是:

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

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

SELECT `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id` 
FROM `app01_city`; SELECT `app01_province`.`id`, `app01_province`.`name`
FROM `app01_province`
WHERE `app01_province`.`id` = 1; SELECT `app01_province`.`id`, `app01_province`.`name`
FROM `app01_province`
WHERE `app01_province`.`id` = 1; SELECT `app01_province`.`id`, `app01_province`.`name`
FROM `app01_province`
WHERE `app01_province`.`id` = 2;

即先去city表中拿到所有数据,然后根据每个city对象关联的province表中的id,分别去province表中拿到对应行的信息。

(2)如果使用select_related()函数:

citys = models.City.objects.select_related().all()
for c in citys:
    print(c.province.name)

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

SELECT `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id`, `app01_province`.`id`, `app01_province`.`name` 
FROM `app01_city`
INNER JOIN `app01_province` ON (`app01_city`.`province_id` = `app01_province`.`id`);

这里可以看到,Django使用了INNER JOIN来获得省份的信息。

使用方法

 函数支持如下三种用法:

*fields 参数

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

例如我们要获得张三的现居省份信息,可以用如下方式:

zhangs = models.Person.objects.select_related('living__province').get(firstname=u"", lastname=u"")  #此处进行了数据库查询操作
zhangs.living.province.name  #此处没有进行数据库查询操作
zhangs.living.province.id  #此处没有进行数据库查询操作

触发的SQL查询如下:

SELECT `app01_person`.`id`, `app01_person`.`firstname`, `app01_person`.`lastname`, `app01_person`.`hometown_id`, `app01_person`.`living_id`, `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id`, `app01_province`.`id`, `app01_province`.`name` 
FROM `app01_person`
INNER JOIN `app01_city` ON (`app01_person`.`living_id` = `app01_city`.`id`)
INNER JOIN `app01_province` ON (`app01_city`.`province_id` = `app01_province`.`id`)
WHERE (`app01_person`.`firstname` = '' AND `app01_person`.`lastname` = '');

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

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

zhangs.hometown.province  #触发数据库查询操作

触发的SQL查询如下:

SELECT `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id` 
FROM `app01_city`
WHERE `app01_city`.`id` = 3; SELECT `app01_province`.`id`, `app01_province`.`name`
FROM `app01_province`
WHERE `app01_province`.`id` = 2;

先找到张三的故乡所在城市,然后根据城市找省份。

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

值得一提的是,从Django 1.7开始,select_related()函数的作用方式改变了。在本例中,如果要同时获得张三的故乡和现居地的省份,在1.7以前你只能这样做:

zhangs = models.Person.objects.select_related('hometown__province', 'living__province').get(firstname=u"", lastname=u"")  #此处进行了数据库查询操作
zhangs.hometown.province.name  #此处没有进行数据库查询操作
zhangs.living.province.name  #此处没有进行数据库查询操作

但是1.7及以上版本,你可以像和queryset的其他函数一样进行链式操作:

zhangs = models.Person.objects.select_related('hometown__province').select_related('living__province').get(firstname=u"",lastname=u"")  #此处进行了数据库查询操作
zhangs.hometown.province.name  #此处没有进行数据库查询操作
zhangs.living.province.name  #此处没有进行数据库查询操作

depth 参数

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

zhangs = models.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”三”)。但要注意两点:

  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失效,只保留最后一个。

 

 

2.1.2  测试实例2

示例1:

ret = models.Employee2.objects.all()  #此处没有涉及数据库查询操作
val1 = ret[0].dept.name  #此处涉及两次数据库查询操作
#共查询两次数据库

#对应的SQL语句:
#1. SELECT `employee2`.`id`, `employee2`.`name`, `employee2`.`age`, `employee2`.`salary`, `employee2`.`province`, `employee2`.`dept_id` FROM `employee2` LIMIT 1;
#2. SELECT `dept2`.`id`, `dept2`.`name` FROM `dept2` WHERE `dept2`.`id` = 1;

即先访问数据库,查到第一个员工对象,然后再去dept2表中查询与第一个员工关联的部门对象的name,共涉及两次查询数据库。

示例2:

ret = models.Employee2.objects.select_related()  #此处不涉及数据库查询操作
print(ret)  #此处涉及一次数据库查询操作 #结果:<QuerySet [<Employee2: 狼爷>, <Employee2: 狼爸>, <Employee2: 学习>, <Employee2: 王二狗>, <Employee2: 大卫>]>

#对应的SQL语句:SELECT `employee2`.`id`, `employee2`.`name`, `employee2`.`age`, `employee2`.`salary`, `employee2`.`province`, `employee2`.`dept_id`, `dept2`.`id`, `dept2`.`name` FROM `employee2` INNER JOIN `dept2` ON (`employee2`.`dept_id` = `dept2`.`id`) LIMIT 21;

即将employee2表和dept2表进行连表,并返回所有数据,QuerySet列表中是多个对象,如果不执行print(ret),并不涉及数据库查询操作。

#对select_related()返回结果操作
val1 = ret[0].name  #涉及一次数据库查询操作  #取第一个对象的名称
val2 = ret[0].dept.name  #涉及一次数据库查询操作  #取第一个对象关联的部门的名称
val3 = ret[0].dept.id  #涉及一次数据库查询操作

即同样拿到第一个员工关联的部门name(例val2),只涉及一次数据库查询操作。

2.2  prefetch_related()

内容来自点我

深入了解点我

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

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

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

(1)如果我们要获得张三所有去过的城市:

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

触发的SQL语句如下:

SELECT `app01_person`.`id`, `app01_person`.`firstname`, `app01_person`.`lastname`, `app01_person`.`hometown_id`, `app01_person`.`living_id` 
FROM `app01_person`
WHERE (`app01_person`.`firstname` = '' AND `app01_person`.`lastname` = ''); SELECT (`app01_person_visitation`.`person_id`) AS `_prefetch_related_val_person_id`, `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id`
FROM `app01_city`
INNER JOIN `app01_person_visitation` ON (`app01_city`.`id` = `app01_person_visitation`.`city_id`)
WHERE `app01_person_visitation`.`person_id` IN (1);

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

(2)或者我们要获得湖北的所有城市名:

hb = models.Province.objects.prefetch_related('city_set').get(name__iexact=u"湖北省")  #'city_set'反向查找,hb是一个province对象,<class 'app01.models.Province'>
for city in hb.city_set.all():
    print(city.name)

触发的SQL语句如下:

SELECT `app01_province`.`id`, `app01_province`.`name` 
FROM `app01_province`
WHERE `app01_province`.`name` LIKE '湖北省'; SELECT `app01_city`.`id`, `app01_city`.`name`, `app01_city`.`province_id`
FROM `app01_city`
WHERE `app01_city`.`province_id` IN (1);

(3)例如要获得所有姓张的人去过的省:

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

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

plist = models.Person.objects.prefetch_related('visitation')  #plist是一个QuerySet列表,包含多个person对象。
ret = [p.visitation.filter(name__icontains=u"") for p in plist]  #注意,最终ret列表中是多个city对象,即p.visitation.是通过一个person对象去查与他关联的所有city对象

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

在Django小于1.7的版本

plist = models.Person.objects.prefetch_related('visitation')
ret = [[city for city in p.visitation.all() if u"" in city.name] for p in plist]  #ret为:[[<City: City object>, <City: City object>]]

在Django>1.7的版本

获取所有人访问过的城市中带有“武”字和“州”的城市:

wus = models.City.objects.filter(name__icontains = u"")
zhous = models.City.objects.filter(name__icontains = u"")
plist = models.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]

None

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

prefetch_cleared_qset = qset.prefetch_related(None)

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()是有效的,之后的将会被无视掉。

2.3  bulk_create批量创建

批量创建多个书籍(book)对象:

objs = [models.Book(title="沙河{}".format(i)) for i in range(150)]
models.Book.objects.bulk_create(objs, 10)  #10代表创建的150个书籍对象,10个一提交保存。
posted @ 2019-09-25 15:55  陌上の花开  阅读(189)  评论(0)    收藏  举报