Django:prefetch_related 没有效果

Posted

技术标签:

【中文标题】Django:prefetch_related 没有效果【英文标题】:Django: prefetch_related has no effect 【发布时间】:2018-07-26 09:52:24 【问题描述】:

我正在尝试使用 prefetch_related 优化数据库查询,但没有成功。

models.py

class Order(models.Model):
    # some fields ...

    @property
    def last_operation(self) -> Optional['OrderOperation']:
        try:
            return self.orderoperation_set.latest()
        except OrderOperation.DoesNotExist:
            return None

    @property
    def total(self) -> Optional[Decimal]:
        last_operation = self.last_operation
        return last_operation.total if last_operation else None

class OrderOperation(TimeStampable, models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)

运行一个shell,可以看出问题:

orders = Order.objects.prefetch_related('orderoperation_set')  # There are 1000 orders
result = sum([order.total for order in orders])
len(connection.queries)
>>> 1003

如我们所见,每个order.total 有一个查询,因此 1000 个查询,这使得整个请求非常糟糕,性能与订单数量成线性关系。

试图了解为什么会发生这种情况,我在prefetch_related Django doc 中找到了这个:

请记住,与 QuerySet 一样,任何暗示不同数据库查询的后续链接方法都将忽略以前缓存的结果,并使用新的数据库查询检索数据。

所以,每次调用latest() 运行一个新查询似乎是正常的。

在这种情况下,您将如何提高性能?(进行几次查询而不是 N,其中 N 是订单数)。

【问题讨论】:

【参考方案1】:

由于 OrderOperation 仅包含单个相关字段 total,因此更好的方法是使用 subquery 注释原始查询中最新操作的总数:

from django.db.models import OuterRef, Subquery
newest = OrderOperation.objects.filter(post=OuterRef('pk')).order_by('-created_at')  # or whatever the timestamp field is
orders = Order.objects.annotate(newest_operation_total=Subquery(newest.values('total')[:1]))

【讨论】:

谢谢@Daniel Roseman,我会尽快实现它。你能看看我自己对这个问题的回答,并与你的解决方案进行比较吗?【参考方案2】:

我在这里发布一个答案,你能告诉我这是否有意义吗?

如果我不调用latest(),而是直接使用[0] 获取查询集中的第一项(或者使用len(qs)-1 获取最后一项,假设order_operations 已经订购了呢?

@property
def last_operation(self) -> Optional['OrderOperation']:
    try:
        qs = self.orderoperation_set.all()
        return qs[len(qs) - 1]
    except IndexError:
        return None

【讨论】:

一个潜在的问题是,当您只需要一个时,您总是从数据库中检索所有操作。这是很多不必要的负担。 你是对的。想一想,知道在现实世界中,orderoperation_set.all() 应该在 95% 的时间返回 1 个结果,并且永远不会超过 3 个,这会是一个可以接受的解决方案吗?

以上是关于Django:prefetch_related 没有效果的主要内容,如果未能解决你的问题,请参考以下文章

Django:prefetch_related 没有效果

prefetch_related 上的 Django ORM 注释

django prefetch_related 很多查询

Django - 过滤 prefetch_related 查询集

Django prefetch_related 与限制

为啥 django 的 prefetch_related() 只适用于 all() 而不是 filter()?