我有一个非常标准的基本社交应用程序 - 每个帖子都有状态更新(即帖子)和多条评论。
鉴于以下简化模型,是否可以使用Django的ORM有效地检索与每个帖子关联的所有帖子和最新的两条评论,而无需执行N + 1个查询? (也就是说,不执行单独的查询来获取页面上每个帖子的最新评论。)
class Post(models.Model):
title = models.CharField(max_length=255)
text = models.TextField()
class Comment(models.Model):
text = models.TextField()
post = models.ForeignKey(Post, related_name='comments')
class Meta:
ordering = ['-pk']
Post.objects.prefetch_related('comments').all()
会抓取所有帖子和评论,但我想在每个帖子中检索有限数量的评论。
更新:
据我所知,如果可以使用Django的ORM完成,可能必须使用某些版本的prefetch_related
。多个查询完全没问题,只要我避免每页进行N + 1次查询。
在Django中处理此问题的典型/推荐方法是什么?
更新2:
使用Django ORM进行简单查询似乎没有直接和简单的方法来有效地完成这项工作。以下答案中有许多有用的解决方案/方法/解决方法,包括:
我不知道哪一个标记为正确,因为我还没有机会尝试所有这些方法 - 但我将赏金授予hynekcer以提供多种选择。
更新3:
我最终使用了@ user1583799的解决方案。
答案 0 :(得分:2)
如果您正在使用Django 1.7,新的Prefetch
对象(允许您自定义预取查询集)可能会有所帮助。
不幸的是,我无法想出一个简单的方法来完全按照你的要求去做。如果您使用的是PostgreSQL并且愿意仅为每篇帖子获取最新评论,则以下内容应该适用于两个查询:
comments = Comment.objects.order_by('post_id', '-id').distinct('post_id')
posts = Post.objects.prefetch_related(Prefetch('comments',
queryset=comments,
to_attr='latest_comments'))
for post in posts:
latest_comment = post.latest_comments[0] if post.latest_comments else None
另一种变体:如果您的评论有时间戳,并且您希望按日期将评论限制为最新评论,那么看起来就像:
comments = Comment.objects.filter(timestamp__gt=one_day_ago)
......然后如上所述。当然,您仍然可以对结果列表进行后期处理,以将显示限制为最多两条注释。
答案 1 :(得分:1)
此解决方案针对内存要求进行了优化,正如您所期望的那样重要。它需要三个查询。第一个查询请求发布,第二个查询仅查询元组(id,post_id)。第三部分是过滤最新评论的详细信息。
from itertools import groupby, islice
posts = Post.objects.filter(...some your flter...)
# sorted by date or by id
all_comments = (Comment.objects.filter(post__in=posts).values('post_id')
.order_by('post_id', '-pk'))
last_comments = []
# the queryset is evaluated now. Only about 100 itens chunks are in memory at
# once during iterations.
for post_id, related_comments in groupby(all_comments(), lambda x: x.post_id):
last_comments.extend(islice(related_comments, 2))
results = {}
for comment in Comment.objects.filter(pk__in=last_comments):
results.setdefault(comment.post_id, []).append(comment)
# output
for post in posts:
print post.title, [x.comment for x in results[post.id]]
但我认为许多数据库后端将第二个和第三个查询合并为一个会更快,因此立即要求所有注释字段。无用的评论将立即被遗忘。
最快的解决方案是使用嵌套查询。该算法与上面的算法类似,但所有内容都是通过原始SQL实现的。它仅限于一些后端,如PostgresQL。
修改强>
我同意这对你没用
...预取将数千条评论加载到内存中,其中99%将不会显示。
因此我编写了相对复杂的解决方案,其中99%将在不加载到内存中的情况下连续读取。
修改强>
A)PostgresQL的嵌套查询
SELECT post_id, id, text FROM
(SELECT post_id, id, text, rank() OVER (PARTITION BY post_id ORDER BY id DESC)
FROM app_comment WHERE post_id in (1, 3, 5)) sub
WHERE rank <= 2
ORDER BY post_id, id
如果我们不相信优化器,则明确要求内存较少。它应该仅从两个内部选择中的索引读取数据,这比从表中的数据少得多。:
SELECT post_id, id, text FROM app_comment WHERE id IN
(SELECT id FROM
(SELECT id, rank() OVER (PARTITION BY post_id ORDER BY id DESC)
FROM app_comment WHERE post_id in (1, 3, 5)) sub
WHERE rank <= 2)
ORDER BY post_id, id
B)使用最早显示的评论的缓存ID
将字段“oldest_displayed”添加到帖子
class Post(models.Model):
oldest_displayed = models.IntegerField()
如果有趣的帖子(您之前按类别等选择了等)过滤pk的评论
过滤
from django.db.models import F
qs = Comment.objects.filter(
post__pk__in=[1, 3, 5],
post__oldest_displayed__lte=F('pk')
).order_by('post_id', 'pk')
pprint.pprint([(x.post_id, x.pk) for x in qs])
嗯,非常好......以及它是如何由Django编译的?
>>> print(qs.query.get_compiler('default').as_sql()[0]) # added white space
SELECT "app_comment"."id", "app_comment"."text", "app_comment"."post_id"
FROM "app_comment"
INNER JOIN "app_post" ON ( "app_comment"."post_id" = "app_post"."id" )
WHERE ("app_comment"."post_id" IN (%s, %s, %s)
AND "app_post"."oldest_displayed" <= ("app_comment"."id"))
ORDER BY app_comment"."post_id" ASC, "app_comment"."id" ASC
最初通过一个嵌套SQL准备所有“oldest_displayed”(并为少于两个注释的帖子设置为零):
UPDATE app_post SET oldest_displayed = 0
UPDATE app_post SET oldest_displayed = qq.id FROM
(SELECT post_id, id FROM
(SELECT post_id, id, rank() OVER (PARTITION BY post_id ORDER BY id DESC)
FROM app_comment ) sub
WHERE rank = 2) qq
WHERE qq.post_id = app_post.id;
答案 2 :(得分:1)
prefetch_related('comments')
将获取帖子的所有评论。
我遇到了同样的问题,数据库是 Postgresql 。我找到了一种方法:
添加额外的字段related_replies
。请注意,FieldType为ArrayField
,支持django1.8dev。我将the code复制到我的项目(django的版本是1.7),只需更改2行即可。(或使用djorm-pg-array)
class Post(models.Model):
related_replies = ArrayField(models.IntegerField(), size=10, null=True)
并使用两个查询:
posts = model.Post.object.filter()
related_replies_id = chain(*[p.related_replies for p in posts])
related_replies = models.Comment.objects.filter(
id__in=related_replies_id).select_related('created_by')[::1] # cache queryset
for p in posts:
p.get_related_replies = [r for r in related_replies if r.post_id == p.id]
收到新评论后,请更新related_replies
。