侧边栏壁纸
博主头像
我的学习心得 博主等级

行动起来,活在当下

  • 累计撰写 223 篇文章
  • 累计创建 60 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

N+1 查询问题

Administrator
2025-01-09 / 0 评论 / 0 点赞 / 9 阅读 / 0 字

prefetch_related

考虑如下两个模型:

class Author(models.Model):
    name = models.CharField(max_length=100)
    
class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

现在遍历所有书籍并访问每本书的作者:

books = Book.objects.all()
for book in books:
    authors = book.authors.all()
    for author in authors:
        print(book.title, author.name)

假设数据库中有 N 本书,上述代码会先执行一次数据库查询获取所有书籍,然后针对每本书籍查询一次作者,总共查询 N+1 次,即 N+1 查询问题

此问题可能出现在 多对多 或者 反向一对多(反向外键)的查询情境中。

Author 表被反复查询了 N 次(每次都需要遍历整张表),那么为什么不将这 N 次查询合并为一次查询呢?这就需要用到 prefetch_related

# 这里将触发两次查询,一次查主表,一次查关联表
books = Book.objects.prefetch_related("authors").all()
for book in books:
    # 这里不会再触发数据库查询,因为数据已经提前取出来了
    authors = book.authors.all()
    for author in authors:
        print(book.title, author.name)

嵌套的预加载

考虑如下三个模型:

class Publisher(models.Model):
    name = models.CharField(max_length=100)
    
class Author(models.Model):
    name = models.CharField(max_length=100)
    publisher = models.ForeignKey(Publisher, models.CASCADE)
    
class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

如果需要访问 Book 查询集中 Book 对象的 .authors 集合,以及 .authors 集合中的 Author 对象关联的 Publisher 对象,可以使用嵌套预加载,只需要三次查询就可以取出所有数据。

# 这里将触发三次查询
books = Book.objects.prefetch_related("authors__publisher").all()
for book in books:
    # 这里不会再触发数据库查询,因为数据已经提前取出来了
    authors = book.authors.all()
    for author in authors:
        # 这里也不会触发查询
        publisher = author.publisher
        print(book.title, author.name, publisher.name)

嵌套对象间使用 __ 隔开即可,多对多关联的用复数,一对一或者外键关联的用单数。

自定义预加载查询集

上述的 Book.objects.prefetch_related("authors").all() 会预加载整个 Author 表,也可以使用 models.Prefetch 预加载部分 Author 数据。

from django.db.models import Prefetch
​
# 这里先对 Author 表执行一次查询,获得较小的查询集,同时可以预加载关联的 Publisher 数据
sub_authors = Author.objects.filter(name__startswith="Wang").select_related("publisher")
​
# 这里访问的 book.authors 已经被预加载了
for book in Book.objects.prefetch_related(Prefetch("authors", queryset=sub_authors)).all():
    for author in book.authors.all():
        print(book.title, author.name, author.publisher.name)

如果自定义查询集 sub_authors 确实已经包含了所有需要访问到的作者,将不再执行查询。

如果某个 book.authors 中存在一个 Author 对象,它不在预加载的查询集 sub_authors 中,当我们实际访问到这个 author 时,还是会触发数据库查询。

所以使用自定义预加载查询集时,请务必确保所有可能用到的关联对象都在预加载查询集中。

Prefetch 的构造函数如下:

Prefetch(lookup: str, queryset: QuerySet[M]=None, to_attr: str=None)

使用 to_attr 可以将预加载的结果附加到指定的属性上(直接新增属性!)而不是已有的属性上。

for book in Book.objects.prefetch_related(Prefetch("authors", queryset=sub_authors, to_attr="wang_authors")).all():
    # for author in book.authors.all():
    for author in book.wang_authors.all():
        print(book.title, author.name, author.publisher.name)

select_related

class Author(models.Model):
    name = models.CharField(max_length=100)
​
class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
for book in Book.objects.all(): # 这里会执行一次查询获取所有 Book 对象
    print(book.title, book.author.name) # 这里会执行一次查询获取 book.author 对象

上面的一对一或者外键关联还是存在 N+1 查询问题。

针对 一对一 或者 外键 关联,我们使用 select_related 处理 N+1 查询问题。

for book in Book.objects.select_related("author").all():
    print(book.title. book.author.name) # 这里不再单独执行查询

同样支持嵌套:

books = Book.objects.select_related("author__publisher").all()

select_related 会减少数据库查询次数,但是会增大第一次查询的工作量,尤其是数据量很大的情况下。

0

评论区