什么是N+1查询问题?
这是一个新手码农特别容易忽视,或者说难以发现的问题,就像笔者在刚刚开始敲代码的时候也会在不知不觉中犯下这个严重影响应用性能的问题!
我们在开发应用的过程中,一个对自己的代码有有追求的程序员总是会想尽一切办法优化程序的性能,影响程序性能的原因有很多,但是N+1问题是一个常见的性能问题,特别是在使用对象关系映射(ORM)框架或进行数据库查询时,往往可能不经意间就犯下了这种错误。
N+1问题就是,在处理一对多或者多对多的关联查询的时候,应用程序首先执行一条查询语句获取主对象的结果集(即“+1”),然后针对结果集中的每一条记录,再执行额外的查询语句以获取其关联的数据,这些额外的查询次数通常与主对象的数量N成正比。因此,总查询次数为N+1,导致大量的数据库查询,从而影响性能。假设有两个表User和Order,其中一个用户(User)可能有多个订单(Order),这是一对多的关系。现在需要查询所有用户的订单,那我们可以先执行一条语句查询出所有用户,然后根据N个用户去查询订单,这里面就总共执行了N+1条查询语句,这就是N+1问题!
N+1查询有什么危害?
N+1问题会导致我们频繁的去访问数据库,严重影响数据库的性能,我们应该尽可能的减少访问数据库的次数。在上述查询方式中每次查询都需要发送到数据库,数据库必须执行查询,然后将结果发送回应用。查询次数越多,获取结果的时间就越长,导致资源严重损耗。
如何避免N+1问题?
1,使用JOIN语句进行查询:通过SQL的JOIN语句,可以在一条查询语句中同时获取主对象和关联对象的信息,从而避免N+1问题。
2,预查询,提前将所有订单信息根据所有用户一次性查询出来,再在代码层面组装对象。
Django中的解决方案
1,select_related
适用于一对一和多对一的关系,内部是使用了sql的join连接,一次性将模型以及其关联的外键模型查出来。
假设我们有以下模型:Person有一个外键hometown关联了City(一个人的居住在哪个城市),而Book又有一个外键author关联了Person(一本书的作者是谁)
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)
假如我们有如下需求,查询出某一本书的作者的居住地。
我们可以先根据id查出这本书,再根据这本书去查出作者,再根据作者去查询他居住的城市
b = Book.objects.get(id=4) # 会有一次查询
p = b.author # 会有一次查询
c = p.hometown # 会有一次查询
如果是按照上述代码,将会引发三次查询!
如果使用select_related进行性能优化,只需要一次查询
b = Book.objects.select_related('author__hometown').get(id=4)
# 一次性查出书的作者以及作者的居住地,django会使用join一次性完成这次查询,
# 整个过程只会有一次查询(注:select_related方法可以传入多个字段的链式连接,
# 多个字段之间以双下划线相连,如author__hometown代表从书本中通过外键作者连
# 接到Person表再从Person中通过hometown连接到City)
p = b.author # 不会查询数据库
c = p.hometown # 不会查询数据库
对于N+1问题:假设我们现在需要根据所有的作者查询出他们的住址。
造成N+1问题的示例代码:
# 先查询出所有作者,会进行一次数据库查询
authors = Person.objects.all()
# 在循环中打印所有作者住处,注意:在每一次循环中都会进行查询,
# 会造成严重性能问题,有N个作者,就会执行N次查询
for author in authors:
print(f"Hometown: {author.hometown.__str__()}"
# 以上代码会造成严重的性能问题!一个会查询N+1次,经典的N+1查询问题!
使用select_related进行性能优化
# 使用select_related一次性查询出所有作者的住处
authors_with_hometowns = Person.objects.select_related('hometown').all()
# 然后可以for循环中遍历查询结果并打印每个作者的住址信息。不会执行查询,
# 只会用到上面的全量查询得到的结果。
for author in authors_with_hometowns:
print(f"Hometown: {author.hometown.__str__()}")
# 以上代码只需要一次查询,完美解决N+1问题
2,prefetch_related
prefetch_related相当于是select_related的升级版,他支持一对多和多对多的关系,因为的底层Django是使用了sql中的join连接的方式进行关联查询,而Django的作者为了避免对多对多的关系进行join连接会导致生成的 SQL 语句非常复杂,特别是当涉及的表非常多或关系链很长时这会严重影响性能。所以Django使用了prefetch_related来处理多对多的关系。对于prefetch_related,首先,Django 会执行一个主查询来获取主模型的实例集合然后对于每个需要预加载的关联关系,Django 会执行一个额外的查询来获取这些关联对象的集合。这些查询是独立的,并且是针对关联模型的,最后在内存中对这些模型进行关联,然后返回。
假设我们有以下模型,Topping配料和Pizza披萨,他们之间是多对多的关系,一个披萨可以用很多种调料,一种调料也可以给多种口味的披萨使用。
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()),
)
如果我们有如下需求,查找出每种披萨所使用的调料:
会造成N+1问题的示例:
# 先查出所有的披萨 这会有一次数据库查询
pizzas = Pizza.objects.all()
# 在for循环中取出每一个披萨的所有调料,然后打印这些调料,如果有N个披萨,
# 就会执行N次查询,又一次造成了N+1查询问题!
for pizza in pizzas:
toppings = pizza.topping.all()
for topping in toppings
print(f"topping: {pizza.topping.__str__()}"
使用prefetch_related进行性能优化:
# 使用prefetch_related查出所有的披萨 这会有一次数据库查询
pizzas = Pizza.objects.all().prefetch_related('toppings')
# 这意味着每一个 Pizza 都有一个 self.toppings.all();现在每次调用
# self.toppings.all() 时,不必再去数据库中查询这些数据,而是在一次
# prefetch_related查询中填充的QuerySet 缓存中找到它们。也就是说,
# 所有相关的调料数据都将在一次查询中被获取,并被用来拼装QuerySets。
# 接下来的每一次获取pizza.topping.all()都不必进行查询
for pizza in pizzas:
toppings = pizza.topping.all()
for topping in toppings
print(f"topping: {pizza.topping.__str__()}"
prefetch_related 需要注意的点
如果有下面这样一段代码,有n条披萨记录,你觉得会执行了几次数据库查询呢?
pizzas = Pizza.objects.prefetch_related('toppings')
[list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]
答案是N+1次,这是为什么,prefetch_related不是可以解决N+1问题吗?请注意这个方法中获取调料记录与之前代码中的不同他多了一个filter(),这意味着每一个pizza.toppings.filter(spicy=True)都是一个新的查询,如果在已经使用prefetch_related
预取了关联对象之后,你执行了一个新的、不同的查询(如.filter()
),这个新的查询将不会利用之前的缓存。这意味着每一次pizza.toppings.filter(spicy=True)都要去数据库进行一次查询,这会严重影响性能!