查询太慢;prefetch_related不能解决问题



我们使用Django 2.1 for 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()

但是在prefetch_related中添加"email_addresses"和"from_user__email_addresses"并不能使页面加载速度更快 - 加载页面大约需要 16 秒。加载页面而不检查每个朋友是否有确认的电子邮件地址时,加载页面大约需要 3 秒。有没有办法一次加载用户的所有电子邮件地址,而不是每次检查用户时?实际上,我也希望朋友查询加载一次,而不是每页加载 96 次(每个用户一次(,但页面在 3 秒内加载,所以没那么重要。但是,如果我能查询一次朋友表,那就更好了。

查询由以下行(链接(引起:

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

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

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

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

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

我想我应该使用post_savepost_delete等信号。

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

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

User.objects.prefetch_related('friends')

或者,您可以使用Prefetch对象来进一步筛选:

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

使用 filter 关键字参数的Count注释会快得多。

from djang.db.models import Count, Q
qs = User.objects.annotate(
friend_count=Count('friends', filter=Q(friends__is_confirmed=True)
)

但是在prefetch_related中添加"email_addresses"和"from_user__email_addresses"并不能使页面加载更快......

那是因为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 = 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 秒。

最新更新