Django 中 N+1 查询问题优化

Django 中 N+1 查询问题优化

Dec 17, 2018
Python, 系统设计, Django

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_relatedprefetch_related 可以减少数据库的查询次数,但是使用起来会有区别,下面分开介绍下。

外键 #

select_related 底层是数据库内连接 inner join 的实现,对于 OneToOneFieldForeignKey 来说,可以提前把外键对应的另一个表的数据 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")

除了可以 join 的 一对一的 OneToOneField 和多对一的 ForeignKey 情况,其实还有多对多和一对多的关系,但是这个情况通过 join 另一个表无法达到唯一的结果,select_related 也就失效了。

于是 Django 提供了 prefetch_related 来通过 Python 的方式解决 join 不能得到确定结果的问题。

qs = Article.objects.prefetch_related("author")

执行的逻辑如下:

  1. select xxx from Article
  2. select xxx from Author where id in (1)
  3. 使用 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 从而避免了对关联表的额外的懒加载查询。

本文共 928 字,上次修改于 Feb 25, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» Django 的中间件执行顺序

» Django 的软删除设计

» 浅谈 Django-REST-Framework 的设计与源码

» Ubuntu 下部署 Django 应用