Django 中 N+1 查询问题优化
12月 17, 2018
Django ORM 框架虽然很好用,但是如果不注意,在查询时很容易引发 N + 1 的查询问题。
N + 1 问题 #
考虑 文章+作者
这样的 Model 层设计,在这个模型中,Author 和 Article 是一对多的关系。
from django.db import models
from django.contrib.auth.models import User
class Author(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=64)
class Article(models.Model):
title = models.CharField(max_length=128)
content = models.TextField()
author = models.ForeignKey(Author)
如果此时想要查询文章的列表,那么大概的查询可能如下:
def article_list():
qs = Article.objects.filter()
ret = []
for item in qs:
ret.append({
"title": item.title,
"author_name": item.author.name
})
return ret
我们知道 Django 的 QuerySet 是惰性执行的,qs 在被赋值后并没有进行实际的数据库查询,这里在 for 循环里通过 item.author.name
获取数据时才真正进行了查询,由此可见,一共进行了 len(qs) + qs 即 N + 1 查询。
其实很多 ORM 都会有这个问题,Django 为了解决 N+1 问题,提供了 select_related
和 prefetch_related
可以减少数据库的查询次数,但是使用起来会有区别,下面分开介绍下。
select_related #
外键 #
select_related
底层是数据库内连接 inner join
的实现,对于 OneToOneField
和 ForeignKey
来说,可以提前把外键对应的另一个表的数据 join 进来,从而减少懒加载的情况。
qs = Article.objects.select_related()
如果 select_related
里不加参数,则会尽可能的加载 Model 层中的外键,即会 join 所有的外键的表,如果 join 的表里有外键会继续 join,所以在知道查询内容的情况下,要尽可能地手动指定想要 join 的表。
外键的外键 #
如果遇到需要 join 两个表才能覆盖的数据,即可能有外键的外键。
qs = Article.objects.select_related("author__user")
深度 depth #
depth 参数可以确定 select_related
的深度,Django 会递归遍历指定深度内的所有的 OneToOneField 或 ForeignKey。
Article.objects.select_related(depth=1)
depth = 1 相当于 Article.objects.select_related("author")
;
depth = 2 相当于 Article.objects.select_related("author__user")
。
prefetch_related #
除了可以 join 的 一对一的 OneToOneField
和多对一的 ForeignKey
情况,其实还有多对多和一对多的关系,但是这个情况通过 join 另一个表无法达到唯一的结果,select_related
也就失效了。
于是 Django 提供了 prefetch_related
来通过 Python 的方式解决 join 不能得到确定结果的问题。
qs = Article.objects.prefetch_related("author")
执行的逻辑如下:
select xxx from Article
select xxx from Author where id in (1)
- 使用 python 合并结果
直观上也能感受到,select_related
的查询效率要高于 prefetch_related
,如果要在查询过程中有需要对 author
有进一步的条件过滤,那么也会触发 QuerySet 的懒加载机制而重新查询数据库,于是 Django 为 prefetch_related
引入了 Prefetch
对象。
Prefetch #
from django.db import models
qs = Article.objects.prefetch_related(
models.Prefetch("Author",
queryset=Author.objects.filter(author_name__startswith="Bob"))
)
使用 Prefetch
从而避免了对关联表的额外的懒加载查询。