写点什么

Django 笔记十一之外键查询优化 select_related 和 prefetch_related

作者:Hunter熊
  • 2023-04-19
    北京
  • 本文字数:2550 字

    阅读完需:约 8 分钟

Django笔记十一之外键查询优化select_related和prefetch_related

本文首发于公众号:Hunter 后端

原文链接:Django笔记十一之外键查询优化select_related和prefetch_related


本篇笔记目录如下:


  1. select_related

  2. prefetch_related


在介绍 select_related 和 prefetch_related 这两个函数前,我们先来看一个例子。


对于,Entry 和 Blog 这两个 model,前面介绍过,Blog 是 Entry 的外键,如下:


class Blog(models.Model):    name = models.CharField(max_length=100)    tagline = models.TextField()

class Entry(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 的 name 字段信息。


我们一般会如此操作:


for entry in Entry.objects.all()[:10]  if entry.blog:    print(entry.blog.name)  else:    print("没有关联 blog 数据")
复制代码


但是这样会有一个问题,那就是,这个 for 循环的操作会查询数据十一次,一次查询 Entry 数据,十次是查询每个 entry_obj 关联的 blog 数据。


这个设计对于系统来说是不合理的,想一想如果我们查询的数据是一千条,一万条,无论是系统接口的等待时间,还是数据库的访问压力,都是不可接受的。


因此我们可以引入 外键 和 ManyToManyTo 的一种能够减少数据库的访问次数的方式:select_related,prefetch_related。

select_related

当我们在使用的时候,如果有需要获取的外键数据,比如 Entry 关联的 Blog 数据,则可以将其字段名作为参数传入,这样在获取数据的时候就可以一次性将所有关联的 Blog 数据也取出来,而不用单独再去查询一遍数据库。


如下,批量操作


for entry in Entry.objects.select_related("blog").all():  print(e.blog)  # 这个操作不会额外再去查询数据库
复制代码


当然也适用于单条数据


e = Entry.objects.get(id=5).select_related("blog")
复制代码


为了验证 select_related() 确实会只查询一遍数据库,有两种方法:一种是在数据库层面打印出来所有查询的 SQL 语句,另一种可以从侧面表示,那就是在系统层面打印出我们的查询条件转化的 SQL 语句。


比如:


Entry.objects.select_related("blog").all().query.__str__()
复制代码


可以看到会输出一个 关联了 Blog 表的 inner join 的 SQL 语句。


SELECT `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` INNER JOIN `blog_blog` ON (`blog_entry`.`blog_id` = `blog_blog`.`id`)
复制代码


链式获取外键数据


比如下面的 model:


class City(models.Model):    pass

class Person(models.Model): hometown = models.ForeignKey( City, on_delete=models.SET_NULL, blank=True, null=True)

class Book(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
复制代码


因为我们在第一步查询的时候,通过双下划线将两个外键字段连接在一起取了出来,所以在第二步和第三步取 Person 数据和 City 数据的时候,就不需要再次查询数据库了。


同时获取多个外键关联字段


如果一个 model 有两个外键字段 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 的操作。


举个例子:


from django.db import models

class Topping(models.Model): name = models.CharField(max_length=30)

class Pizza(models.Model): name = models.CharField(max_length=50) toppings = models.ManyToManyField(Topping)

def __str__(self): return "%s (%s)" % ( self.name, ", ".join(topping.name for topping in self.toppings.all()), )
复制代码


当我们执行:


Pizza.objects.all()
复制代码


的时候,因为每一条 Pizza 数据实例化的时候,都会调用 str() 函数,而这个函数会再次去请求一遍数据库,所以多条 Pizza 数据会导致查询多次数据库。


因为我们可以使用 prefetch_related() 函数来达到减少查询的目的:


Pizza.objects.prefetch_related('toppings').all()
复制代码


这样的话,对数据库的查询会减少到两次,一次是查询出所有的 Pizza 数据,一次是根据所有的 pizza_id 找到所有关联的 topping 数据。


如果有兴趣,可以比对下面两条语句在 shell 中执行的时候,MySQL 服务器接收到的 SQL 查询语句:


Pizza.objects.all()
Pizza.objects.prefetch_related('toppings').all()
复制代码


下面一种情况需要注意哦:


pizzas = Pizza.objects.prefetch_related('toppings')[list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]
复制代码


因为第二步操作里,会对 toppings 数据进行一次新的 filter 过滤操作,所以会导致每次该语句重新去查询数据库,也就是说,我们的 prefetch_related() 操作是失效的。


以上就是本篇笔记全部内容,接下来会介绍查询里的 defer 和 only 函数。

发布于: 刚刚阅读数: 3
用户头像

Hunter熊

关注

公众号:Hunter后端 2018-09-17 加入

Python后端工程师,欢迎互相沟通交流

评论

发布
暂无评论
Django笔记十一之外键查询优化select_related和prefetch_related_Python_Hunter熊_InfoQ写作社区