You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django中预取关联模型的最新对象以避免N+1查询?

解决Django中使用latest()时的N+1查询问题

问题背景

现有Django模型如下:

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

执行以下代码时会出现预期的N+1查询问题:

queryset = Author.objects.all()
for author in queryset:
    print(author.name)
    print(author.books.latest("created_at").title)

尝试通过queryset = Author.objects.prefetch_related('books')预取books关联,但未解决N+1问题——原因是预取执行的是SELECT * FROM book WHERE author_id IN (1,2,...),而latest()执行的是SELECT * FROM book WHERE author_id = 1 ORDER BY created_at DESC LIMIT 1,二者逻辑不同。

也尝试了queryset = Author.objects.prefetch_related(Prefetch('books', queryset=Book.objects.order_by("-created_at"))),但同样无效。

请问应如何编写预取语句,才能在使用latest()时避免N+1查询?


解决方案

要解决这个问题,核心是预取每个作者的最新单本图书,而非全部关联图书。以下两种方法可以实现:

方法一:子查询+预取指定图书ID

先通过子查询获取每个作者最新图书的ID,再仅预取这些ID对应的图书:

from django.db.models import Subquery, OuterRef
from django.db.models import Prefetch

# 子查询:获取每个作者最新的单本图书ID
latest_book_subquery = Book.objects.filter(
    author=OuterRef('pk')
).order_by('-created_at').values('pk')[:1]

# 先给作者标注最新图书ID,再预取对应图书到自定义属性
queryset = Author.objects.annotate(
    latest_book_id=Subquery(latest_book_subquery)
).prefetch_related(
    Prefetch(
        'books',
        queryset=Book.objects.filter(pk__in=Subquery(latest_book_subquery)),
        to_attr='latest_book'
    )
)

# 使用时直接从预取的集合中取最新图书
for author in queryset:
    print(author.name)
    print(author.latest_book[0].title)

方法二:窗口函数筛选最新图书

利用Django的窗口函数,给每个作者的图书按创建时间倒序编号,仅保留编号为1的最新图书,再预取这些结果:

from django.db.models import Window, F
from django.db.models.functions import RowNumber
from django.db.models import Prefetch

# 用窗口函数筛选每个作者的最新图书
latest_books_queryset = Book.objects.annotate(
    row_num=Window(
        partition_by=F('author'),  # 按作者分组
        order_by=F('created_at').desc()  # 按创建时间倒序排序
    )
).filter(row_num=1)  # 只取每组的第一本(最新的)

# 预取筛选后的最新图书到自定义属性
queryset = Author.objects.prefetch_related(
    Prefetch(
        'books',
        queryset=latest_books_queryset,
        to_attr='latest_book'
    )
)

# 使用方式一致
for author in queryset:
    print(author.name)
    print(author.latest_book[0].title)

为什么之前的方法无效?

之前的预取操作只是把该作者的全部图书(或排序后的全部图书)加载到内存,但author.books.latest()是触发数据库层面的查询,不会直接使用内存中预取的集合计算。而上面的方法是直接在数据库层面筛选出每个作者的最新图书,再预取到内存,循环时直接从预取的集合中读取,不会触发新的数据库查询。


内容的提问来源于stack exchange,提问作者Monoab

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 05:40:22