当前位置: 首页 > 后端技术 > Python

DjangoNote11外键查询优化select_related和prefetch_related

时间:2023-03-25 21:10:54 Python

本篇笔记内容如下:select_relatedprefetch_related在介绍select_related和prefetch_related这两个函数之前,先来看一个例子。对于Entry和Blog这两个模型,前面说了Blog是Entry的外键,如下:classBlog(models.Model):name=models.CharField(max_length=100)tagline=models.TextField()classEntry(models.Model):blog=models.ForeignKey(Blog,on_delete=models.CASCADE)headline=models.CharField(max_length=255)body_text=models.TextField()pub_date=models.DateField()mod_date=models.DateField()authors=models.ManyToManyField(Author)number_of_comments=models.IntegerField()number_of_pingbacks=models.IntegerField()rating=models.IntegerField()例如我们需要获取Entry的前十个条目,然后打印出名字关联Blog的字段信息。我们一般是这样做的:forentryinEntry.objects.all()[:10]ifentry.blog:print(entry.blog.name)else:print("Noblogdataassociated")但是这样会有问题this,也就是这个for循环的操作会查询11次数据,一次查询Entry数据,10次查询每个entry_obj关联的blog数据。这种设计对系统来说是不合理的。想一想,如果我们查询的数据是1000条或者10000条,无论是系统接口的等待时间,还是数据库的访问压力,都是无法接受的。因此,我们可以引入外键和ManyToManyTo,一种减少数据库访问次数的方法:select_related,prefetch_related。在使用select_related的时候,如果有需要获取的外键数据,比如Entry关联的Blog数据,可以将其字段名作为参数传入,这样获取数据的时候就可以一次性获取到所有相关的Blog了。数据也取出来了,不用单独再去查询数据库。如下,批量操作:forentryinEntry.objects.select_related("blog").all():print(e.blog)#这个操作不会额外查询数据库,当然单件也适用数据:e=条目。objects.get(id=5).select_related("blog")为了验证select_related()只会查询一次数据库,有两种方法:一种是打印出数据库级别的所有查询SQL语句,以及另一个是从侧面来说,就是把我们的查询条件在系统层面转换出来的SQL语句打印出来。例如:Entry.objects.select_related("blog").all().query.__str__()可以看到它会输出一条与Blog表的内连接关联的SQL语句。选择`blog_entry`.`id`,`blog_entry`.`blog_id`,`blog_entry`.`headline`,`blog_entry`.`body_text`,`blog_entry`.`pub_date`,`blog_entry`.`mod_date`,`blog_entry`.`number_of_comments`,`blog_entry`.`number_of_pingbacks`,`blog_entry`.`rating`,`blog_blog`.`id`,`blog_blog`.`name`,`blog_blog`.`tagline`FROM`blog_entry`INNERJOIN`blog_blog`ON(`blog_entry`.`blog_id`=`blog_blog`.`id`)链获取外键数据例如下面的model:classCity(models.Model):passclassPerson(models.Model):hometown=models.ForeignKey(City,on_delete=models.SET_NULL,blank=True,null=True)classBook(models.Model):author=models.ForeignKey(Person,on_delete=models.CASCADE)我们可以使用下面的查询与Book关联的Person和与Person数据关联的City数据的语句:book=Book.objects.select_related("author__hometown").get(id=4)person=book.authorcity=person.hometown因为我们是in第一步查询时,两个外键字段ds通过双下划线连接在一起,所以在第二步和第三步取Person数据和City数据时,不需要再次查询数据库。同时获取多个外键相关字段。如果一个模型有两个外键字段foo和bar,下面两种写法会提取这两个外键字段的关联:select_related("foo","bar")select_related("foo").select_related("bar")需要注意的是,这个链式操作与order_by()的结果不同。上面提到的order_by()的链式操作会导致后面的覆盖前面的,但是在取外键数据的时候,会同时去取。注意:select_related()仅适用于ForeignKey和OneToOne。如果是ManyToMany字段,则需要使用下面的prefetch_related()函数。prefetch_related()prefetch_related()和select_related()功能相似,都是通过减少查询次数来实现查询优化。但是prefetch_related()用于ManyToMany操作。例如:fromdjango.dbimportmodelsclassTopping(models.Model):name=models.CharField(max_length=30)classPizza(models.Model):name=models.CharField(max_length=50)toppings=models.ManyToManyField(Topping)def__str__(self):return"%s(%s)"%(self.name,",".join(topping.namefortoppinginself.toppings.all()),)当我们执行:在Pizza.objects.all()时,由于实例化了每条Pizza数据,都会调用__str__()函数,该函数会再次请求数据库,所以多条Pizza数据会导致多次数据库查询。因为我们可以使用prefetch_related()函数来达到减少查询的目的:Pizza.objects.prefetch_related('toppings').all()这样的话,对数据库的查询就会减少为两种,一种是查询所有Pizza数据,一次根据所有pizza_id查找所有关联的浇头数据。有兴趣的可以对比一下在shell中执行如下两条语句时MySQL服务器收到的SQL查询语句:Pizza.objects.all()Pizza.objects.prefetch_related('toppings').all()你需要注意这种情况:pizzas=Pizza.objects.prefetch_related('toppings')[list(pizza.toppings.filter(spicy=True))forpizzainpizzas]因为在第二步中,toppings数据会被processedoncenewfilter过滤操作会导致每次语句都要重新查询数据库,也就是说我们的prefetch_related()操作是无效的。以上就是这篇笔记的全部内容,接下来我们将介绍查询中的defer和only函数。