Django prefetch_related 与限制

Posted

技术标签:

【中文标题】Django prefetch_related 与限制【英文标题】:Django prefetch_related with limit 【发布时间】:2014-09-01 16:00:58 【问题描述】:

有没有办法告诉prefetch_related 只获取一组有限的相关对象?假设我正在获取用户列表,并且我知道我想获取他们最近的 cmets。我没有在循环中为每个用户获取 cmets,而是使用 prefetch_related 在获取用户时预取它们。我的理解是,这将获取原始查询结果中存在的任何用户创建的所有 cmets,但我只想为每个用户显示最新的 5 个。

如果 cmets 列表真的很大,这会如何影响性能?有没有办法在单个(或 2 个)查询中为每个用户仅获取 5 个 cmets?它不必与用于获取用户的原始查询相同,但这很好。

我本质上是想转这个

   users = User.objects.all()
   for user in users:
       user.comments.all()[:10]

变成这样的

 User.objects.all().prefetch_related('comments', limit=10)

因此,如果用户有 100 或 10000 个 cmets,它们不会全部加载到内存中。你会如何在原始 SQL 中做这样的事情?

【问题讨论】:

我不认为使用预取是做这件事的好方法。事实上 prefetch_related 对每个关系进行单独的查找,并在 Python 中进行连接。这意味着您将在 python 中预加载 cmets,并且连接将从这个预加载的 cmets 列表中完成。在您的情况下,为了确保每个用户的最后 10 个 cmets,您需要预加载所有这些用户 我可以忍受每个关系一个查询,但每个对象一个查询才是真正的杀手。 我同意,每个对象一个查询是一场噩梦。但是为什么不直接做:users = User.objects.all().prefetch_related('comments') 在这种情况下,您将只执行 2 个查询 如果 cmets 表有数十万行与选中的用户相关联怎么办?用户评论关系不太可能出现,但在其他情况下很有可能。我担心为您选择的每 10 到 20 个用户(考虑分页)获取所有数百或数千个 cmets 并将它们加入 python 会出现性能问题。 在这种情况下,我最好的办法可能是在 redis 之类的东西中缓存排名靠前的 commet,或者对排名前 10 的 cmets 进行非规范化。 【参考方案1】:

我认为现在在 django 新版本中有一个解决方法,因为我们有 OuterRef 和 Subquery。

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

subqry = Subquery(Comment.objects \
    .filter(user_id=OuterRef('user_id')) \
    .values_list('id', flat=True)[:5])

User.objects.prefetch_related(
    Prefetch('comments', queryset=Comment.objects.filter(id__in=subqry)))

【讨论】:

你看到horrible它生成的sql了吗?你用它检查过性能吗? @deathangel908 我不确定是否可以生成更好的 sql 以在主结果列表下实现嵌套的有限行。我刚刚想出了一种在 Django ORM 中实现嵌套查询限制的方法。如果您想到任何更好的原始 sql,请分享我们也许能够找到一种方法来为其生成 Django ORM 代码。 请看***.com/questions/56573615/… @haseebahmad 如果我需要限制传递给 prefetch_related_objects 函数的预取,我该如何使用这个技巧? @Desh 我认为您可以将相同的“Prefetch('cmets', queryset=Comment.objects.filter(id__in=subqry))”作为第二个参数传递给 prefetch_related_objects。尚未测试,但我认为它会工作【参考方案2】:

限制预取相关对象数量的唯一方法似乎是使用 Prefetch() 和过滤文件。使用切片

User.objects.all().prefetch_related(
    Prefetch('msg_sent', queryset=UserMsg.objects.order_by('-created')[:10]))

返回错误

AssertionError: Cannot filter a query once a slice has been taken.

限制相关对象数量的唯一方法似乎是对一个值使用过滤器,例如

from datetime import datetime, timedelta
timelimit = datetime.now() - timedelta(days=365)

User.objects.all().prefetch_related(
    Prefetch('msg_sent', queryset=UserMsg.objects.filter(created__gte=timelimit)))

虽然它不会返回一个固定的数字,但 in 在某些情况下可能很有用,它会减少预取对象的数量。

【讨论】:

这是一张关于 Prefetch 对象不接受带有切片的查询集的票:code.djangoproject.com/ticket/26780【参考方案3】:

这就是对我真正有用的 django(2.1)(基于haseebahmad 答案)。 为了让 prefetch_related 接受自定义查询集:Prefetch 所以:

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

User.objects.all().prefetch_related(Prefetch('comment_set',  
queryset=Comment.objects.filter(id__in= 
Subquery(Comment.objects.filter(user_id=OuterRef('user_id')).
values_list('id', flat=True)[:1]))))

【讨论】:

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

Django中select_related和prefetch_related的用法与区别

django prefetch_related 是不是应该与 GenericRelation 一起使用

Django prefetch_related 一个大型数据集

Django prefetch_related缓存不反映更改

如何使用 prefetch_related 获取 Django 相关模型中的最新位置

django- 在另一个 prefetch_related 中使用 prefetch_related