Django 中 N+1 查询问题优化
12月 17, 2018
注意:此文章已超过 1 年未更新,部分信息可能已经过时。
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 Articleselect 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 从而避免了对关联表的额外的懒加载查询。