【问题标题】:Queries are too slow; prefetch_related not solving the problem查询太慢; prefetch_related 没有解决问题
【发布时间】:2020-01-03 11:59:48
【问题描述】:

我们将 Django 2.1 用于Speedy Net。我的页面每页显示大约 96 个用户,对于每个用户,我想显示他在 Speedy Match 上有多少朋友,以及一个有效的电子邮件地址。如果(self.email_addresses.filter(is_confirmed=True).exists()) 为真,查询会检查每个用户:

def has_confirmed_email(self):
    return (self.email_addresses.filter(is_confirmed=True).exists())

对于 96 个用户中的每个用户,它会检查他的所有朋友并运行此查询 - 每页超过数百次。获取用户的查询是User.objects.all().order_by(<...>),然后对每个用户检查这个查询:

qs = self.friends.all().prefetch_related("from_user", "from_user__{}".format(SpeedyNetSiteProfile.RELATED_NAME), "from_user__{}".format(SpeedyMatchSiteProfile.RELATED_NAME), "from_user__email_addresses").distinct().order_by('-from_user__{}__last_visit'.format(SiteProfile.RELATED_NAME))

我在用户管理器模型中添加了prefetch_related

def get_queryset(self):
    from speedy.net.accounts.models import SiteProfile as SpeedyNetSiteProfile
    from speedy.match.accounts.models import SiteProfile as SpeedyMatchSiteProfile
    return super().get_queryset().prefetch_related(SpeedyNetSiteProfile.RELATED_NAME, SpeedyMatchSiteProfile.RELATED_NAME, "email_addresses").distinct()

但是将“email_addresses”和“from_user__email_addresses”添加到prefetch_related 并不能加快页面加载速度 - 加载页面大约需要 16 秒。当加载页面而不检查每个朋友是否有确认的电子邮件地址时,加载页面大约需要 3 秒。有没有办法可以一次加载用户的所有电子邮件地址,而不是每次检查用户时?实际上,我还希望朋友查询被加载一次,而不是每页 96 次(每个用户一次),但是页面在 3 秒内加载,所以没关系。但如果我能查询一下朋友表就更好了。

查询是由以下行(link)引起的:

if ((self.user.has_confirmed_email()) and (step >= self.activation_step)):

这是由is_active_and_valid 调用的get_matching_rank 调用,以检查用户是否与特定用户匹配。这是由模型中的方法get_friends 调用的。

更新 #1:如果我在模型中将 def has_confirmed_email(...) 更改为 return True,页面加载速度仅快 3 秒(13 秒而不是 16 秒),因此可能会有更多与性能相关的问题此页面中的问题。

如果我禁用get_matching_rank 的功能并将其替换为普通的return 5,页面加载速度会更快。但是我们当然需要这个函数的功能。也许我们可以在为两个特定用户集调用此函数时将结果缓存几分钟?

更新#2:我想在用户模型中添加一个布尔字段,如果用户有一个确认的电子邮件地址,这将是真的。每次保存或删除电子邮件地址时,都会更新此字段。我知道如何覆盖保存方法,但是当电子邮件地址被删除时如何更新此字段?它也可能被管理员删除。

我认为我应该使用post_savepost_delete 等信号。

【问题讨论】:

  • 为此实现某种缓存或中间存储会更好吗?您为此询问了很多数据库,可能是时候寻找此类数据的替代品了
  • @Jason 是的,我同意,电子邮件地址可能会被确认或删除,只有这些时候has_confirmed_email 的值会发生变化。无法使已确认的电子邮件地址变为未确认。

标签: python django query-performance prefetch


【解决方案1】:

要使预取产生任何效果,您必须在用户模型上使用它 - 很难从包含的内容中判断您是否正在这样做。

如果没有为每个用户预取朋友,self.friends.all() 将导致查询。要使用预取绕过查询,您可以执行以下操作之一:

User.objects.prefetch_related('friends')

或者您可以使用Prefetch 对象进一步过滤:

User.objects.prefetch_related(Prefetch(
    'friends',
    queryset=Friend.objects.filter(is_confirmed=True)
)

使用过滤器关键字参数的Count 注释会更快。

from djang.db.models import Count, Q

qs = User.objects.annotate(
    friend_count=Count('friends', filter=Q(friends__is_confirmed=True)
)

【讨论】:

  • 电子邮件地址检查必须来自代码,因为它还会检查其他内容,例如用户之间的匹配。但仅从数据库中检索到电子邮件地址。
  • 我想在不更改代码的情况下从数据库中检索一次相关用户的所有电子邮件地址。
  • 无法确认好友,但可以确认或取消确认每个电子邮件地址。
  • @Uri 很难在没有看到你的模型的情况下分辨出来,但我猜有一个朋友链接到一个电子邮件地址?在这种情况下,在您的 Prefetch 对象中,您可以使用 .filter(email_address__is_confirmed=True) 过滤查询集
  • 对于每个用户,我可以查询(self.email_addresses.filter(is_confirmed=True).exists()) 中的电子邮件地址,但我想为每个用户获取一次所有电子邮件地址,而不是一次。朋友就是用户。
【解决方案2】:

但是将“email_addresses”和“from_user__email_addresses”添加到prefetch_related 不会使页面加载更快...

那是因为self.email_addresses.filter(is_confirmed=True).exists() 没有使用预取的QuerySet

要使用预取的self.email_addresses,在内存中过滤:

def has_confirmed_email(self):
    if self.email_addresses.all()._result_cache is not None:
        return any(email_address.is_confirmed for email_address in self.email_addresses.all())

    return (self.email_addresses.filter(is_confirmed=True).exists())

注意:如果没有预取,那么改进后的实现仍然会在每次 has_confirmed_email 函数调用时访问数据库,因为 .filter 仍然会创建一个新的 QuerySet。要处理这个问题,请将 has_confirmed_email 设为 Django @cached_property

说明

来自https://docs.djangoproject.com/en/3.0/ref/models/querysets/#prefetch-related

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

>>> pizzas = Pizza.objects.prefetch_related('toppings')
>>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

...预取的缓存在这里无能为力;事实上,它会损害性能,因为您已经完成了一个未使用的数据库查询。所以请谨慎使用此功能!

【讨论】:

  • 是的,我明白了。谢谢你。但我已经在数据库中添加了has_confirmed_email 作为字段。请在下面查看我的答案。
  • 其实return any(email_address.is_confirmed for email_address in self.email_addresses)无论如何都可以工作,不管查询是否被预取。
  • 是的,我在发布后看到了您的答案(我输入了答案,不得不离开,然后在几个小时后发布)。此答案解决了问题中的原始问题(查询太慢; prefetch_related 未解决问题),而您的答案以非常不同的方式实现了最终目标(使页面加载更快)。
  • 尽管return any(...) 可以在查询是否被预取的情况下工作,但它会产生性能成本——将所有email_addresses 加载到内存中——而不是.exists()
  • 单个用户的电子邮件地址不应超过 5 个,因此我认为这两种方式所需的时间应该差不多。但是,如果一次为多个用户预取电子邮件地址,使用它会更快。
【解决方案3】:

我在 User 模型中添加了一个字段:

has_confirmed_email = models.BooleanField(default=False)

以及方法:

def _update_has_confirmed_email_field(self):
    self.has_confirmed_email = (self.email_addresses.filter(is_confirmed=True).count() > 0)
    self.save_user_and_profile()

还有:

@receiver(signal=models.signals.post_save, sender=UserEmailAddress)
def update_user_has_confirmed_email_field_after_saving_email_address(sender, instance: UserEmailAddress, **kwargs):
    instance.user._update_has_confirmed_email_field()


@receiver(signal=models.signals.post_delete, sender=UserEmailAddress)
def update_user_has_confirmed_email_field_after_deleting_email_address(sender, instance: UserEmailAddress, **kwargs):
    instance.user._update_has_confirmed_email_field()

在用户模型中:

def delete(self, *args, **kwargs):
    if ((self.is_staff) or (self.is_superuser)):
        warnings.warn('Can’t delete staff user.')
        return False
    else:
        self.email_addresses.all().delete() # This is necessary because of the signal above.
        return super().delete(*args, **kwargs)

我还从管理员视图中删除了好友数,现在管理员视图页面加载时间约为 1.5 秒。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-22
    • 2018-09-12
    • 2014-01-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多