1 from django.db import models 2 3 # 省份 4 class Province(models.Model): 5 name = models.CharField(max_length=10) 6 7 # 城市 8 class City(models.Model): 9 name = models.CharField(max_length=5) 10 province = models.ForeignKey(Province) 11 12 # 人 13 class Person(models.Model): 14 name = models.CharField(max_length=20) 15 # 去过的城市 16 visitation = models.ManyToManyField(City, related_name="visitor") 17 # 故乡 18 hometown = models.ForeignKey(City, related_name="birth") 19 # 居住地 20 living = models.ForeignKey(City, related_name="citizen")
province表:
city表:
person表:
示例模型是用来记录各我的的故乡、居住地、以及到过的城市。sql
对于一对一字段(OneToOneField)和外键(多对一)字段(ForeignKey),可使用select_related 来对QuerySet进行优化。数据库
1 city_list = models.City.objects.all() 2 [print('{}=>{}'.format(city.name, city.province.name)) for city in city_list] 3 4 ''' 5 result: 6 武汉市=>湖北省 7 孝感市=>湖北省 8 广州市=>广东省 9 深圳市=>广东省 10 '''
1 (0.000) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city`; args=() 2 (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,) 3 (0.001) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,) 4 (0.001) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 2; args=(2,) 5 (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 2; args=(2,)
这样会致使线性的SQL查询,若是对象数量n太多,每一个对象中有k个外键字段的话,就会致使n*k+1次SQL查询。在本例中,由于有4个city对象就致使了5次SQL查询。django
1 city_list = models.City.objects.select_related().all() 2 [print('{}=>{}'.format(city.name, city.province.name)) for city in city_list] 3 4 ''' 5 result: 6 武汉市=>湖北省 7 孝感市=>湖北省 8 广州市=>广东省 9 深圳市=>广东省 10 '''
1 (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=()
在对QuerySet使用select_related()函数后,Django会一次性获取相应外键对应的对象,从而在以后须要的时候没必要再查询数据库了。这里咱们能够看到,Django使用了INNER JOIN来得到省份的信息。缓存
select_related() 接受可变长参数,每一个参数是须要获取的外键(父表的内容)的字段名,以及外键的外键的字段名、外键的外键的外键...。若要选择外键的外键须要使用两个下划线“__”来链接。例如咱们要得到张三的现居省份,能够用以下方式:ide
1 p = models.Person.objects.select_related('living__province').get(name='张三') 2 print(p.living.province.name) 3 4 ''' 5 result: 6 广东省 7 '''
1 (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=('张三',)
能够看到,Django使用了2次 INNER JOIN 来完成请求,得到了city表和province表的内容并添加到结果表的相应列,这样在调用p.living的时候也没必要再次进行SQL查询。函数
然而,未指定的外键则不会被添加到结果中。这时候若是须要获取张三的故乡就会进行SQL查询了:性能
1 p = models.Person.objects.select_related('living__province').get(name='张三') 2 print(p.hometown.province.name) 3 4 ''' 5 result: 6 湖北省 7 '''
1 (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=('张三',) 2 (0.000) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` WHERE `city`.`id` = 2; args=(2,) 3 (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` = 1; args=(1,)
若是不指定外键,就会进行两次查询。若是深度更深,查询的次数更多。fetch
select_related()也能够不加参数,这样表示要求Django尽量深的select_related。例如:优化
1 p = models.Person.objects.select_related().get(name='张三') 2 print(p.hometown.province.name) 3 print(p.living.province.name) 4 5 ''' 6 result: 7 湖北省 8 广东省 9 '''
1 (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=('张三',)
注意:spa
Django自己内置一个上限,对于特别复杂的表关系,Django可能在你不知道的某处跳出递归,从而与你想的作法不同。
Django并不知道你实际要用的字段有哪些,因此会把全部的字段都抓进来,从而会形成没必要要的浪费而影响性能。
对于多对多字段(ManyToManyField)和一对多字段,可使用prefetch_related()来进行优化。或许你会说,没有一个叫OneToManyField的东西啊。实际上,使用ForeignKey的字段就是一个多对一的字段,而被ForeignKey关联的字段就是一对多字段了。prefetch_related()和select_related()的设计目的很类似,都是为了减小SQL查询的数量,可是实现的方式不同。后者是经过JOIN语句,在SQL查询内解决问题。可是对于多对多关系,使用SQL语句解决就显得有些不太明智,由于JOIN获得的表将会很长,会致使SQL语句运行时间的增长和内存占用的增长。prefetch_related()的解决方法是,分别查询每一个表,而后用Python处理他们之间的关系。
1 p = models.Person.objects.prefetch_related('visitation').get(name='张三') 2 [print(c.name) for c in p.visitation.all()] 3 ''' 4 result: 5 武汉市 6 孝感市 7 广州市 8 深圳市 9 '''
1 (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person` WHERE `person`.`name` = '张三'; args=('张三',) 2 (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,)
1 hb = models.Province.objects.prefetch_related('city_set').get(name__iexact=u"湖北省") 2 for city in hb.city_set.all(): 3 print(city.name) 4 ''' 5 result: 6 武汉市 7 孝感市 8 '''
1 (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`name` LIKE '湖北省'; args=('湖北省',) 2 (0.001) SELECT `city`.`id`, `city`.`name`, `city`.`province_id` FROM `city` WHERE `city`.`province_id` IN (1); args=(1,)
和select_related()同样,prefetch_related()也支持深度查询,例如要得到全部姓张的人去过的省:
1 p_list = models.Person.objects.filter(name__iexact='张三').prefetch_related('visitation__province').all() 2 for i in p_list: 3 for city in i.visitation.all(): 4 print(city.province.name) 5 ''' 6 result: 7 湖北省 8 湖北省 9 广东省 10 广东省 11 '''
1 (0.001) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person` WHERE `person`.`name` LIKE '张三'; args=('张三',) 2 (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,) 3 (0.000) SELECT `province`.`id`, `province`.`name` FROM `province` WHERE `province`.`id` IN (1, 2); args=(1, 2)
要注意的是,在使用QuerySet的时候,一旦在链式操做中改变了数据库请求,以前用prefetch_related缓存的数据将会被忽略掉。这会致使Django从新请求数据库来得到相应的数据,从而形成性能问题。这里提到的改变数据库请求指各类filter()、exclude()等等最终会改变SQL代码的操做。而all()并不会改变最终的数据库请求,所以是不会致使从新请求数据库的。举个例子,要获取全部人访问过的城市中带有“市”字的城市,这样作会致使大量的SQL查询:
1 plist = models.Person.objects.prefetch_related('visitation') 2 l = [p.visitation.filter(name__icontains=u"市") for p in plist] 3 for i in l: 4 for j in i: 5 print(j.name) 6 ''' 7 result: 8 武汉市 9 孝感市 10 广州市 11 深圳市 12 '''
1 (0.000) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person`; args=() 2 (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,) 3 (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, '%市%')
由于数据库中有1人,致使了2+1次SQL查询。详细分析一下这些请求事件。众所周知,QuerySet是lazy的,要用的时候才会去访问数据库。运行到第二行Python代码时,for循环将plist看作iterator,这会触发数据库查询。最初的两次SQL查询就是prefetch_related致使的。虽然已经查询结果中包含全部所需的city的信息,但由于在循环体中对Person.visitation进行了filter操做,这显然改变了数据库请求。所以这些操做会忽略掉以前缓存到的数据,从新进行SQL查询。可是若是有这样的需求了应该怎么办呢?能够在Python中完成这部分操做:
1 plist = models.Person.objects.prefetch_related('visitation') 2 [[print(city.name) for city in p.visitation.all() if u"市" in city.name] for p in plist] 3 ''' 4 result: 5 武汉市 6 孝感市 7 广州市 8 深圳市 9 '''
1 (0.001) SELECT `person`.`id`, `person`.`name`, `person`.`hometown_id`, `person`.`living_id` FROM `person`; args=() 2 (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,)