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缓存不反映更改