在数据库有外键的时候,使用 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查询得到的结果如下:
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=”三”)。但要注意两点:
- Django本身内置一个上限,对于特别复杂的表关系,Django可能在你不知道的某处跳出递归,从而与你想的做法不一样.
- Django并不知道你实际要用的字段有哪些,所以会把所有的字段都抓进来,从而会造成不必要的浪费而影响性能.
小结
- select_related主要针一对一和多对一关系进行优化。
- select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
- 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
- 也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
- 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。
- 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对象
- 一个Prefetch对象只能指定一项prefetch操作。
- Prefetch对象对字段指定的方式和prefetch_related中的参数相同,都是通过双下划线连接的字段名完成的。
- 可以通过 queryset 参数手动指定prefetch使用的QuerySet。
- 可以通过 to_attr 参数指定prefetch到的属性名。
- Prefetch对象和字符串形式指定的lookups参数可以混用。
小结
- prefetch_related主要针一对多和多对多关系进行优化。
- prefetch_related通过分别获取各个表的内容,然后用Python处理他们之间的关系来进行优化。
- 可以通过可变长参数指定需要select_related的字段名。指定方式和特征与select_related是相同的。
- 在Django >= 1.7可以通过Prefetch对象来实现复杂查询,但低版本的Django好像只能自己实现。
- 作为prefetch_related的参数,Prefetch对象和字符串可以混用。
- prefetch_related的链式调用会将对应的prefetch添加进去,而非替换,似乎没有基于不同版本上区别。
- 可以通过传入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()。
总结:
- 因为select_related()总是在单次SQL查询中解决问题,而prefetch_related()会对每个相关表进行SQL查询,因此select_related()的效率通常比后者高。
- 鉴于第一条,尽可能的用select_related()解决问题。只有在select_related()不能解决问题的时候再去想prefetch_related()。
- 你可以在一个QuerySet中同时使用select_related()和prefetch_related(),从而减少SQL查询的次数。
- 只有prefetch_related()之前的select_related()是有效的,之后的将会被无视掉。