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

在数据库有外键的时候,使用 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.name


class 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_name


class 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查询得到的结果如下:

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

函数支持如下三种用法:
*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查询。

idfirst_namelast_namefavorite_book_ididbook_namepublish_ididname
111钢铁是怎样炼成的11人民日报出版社

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

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()是有效的,之后的将会被无视掉。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值