2014-10-12 17 views
4

我有一个非常标准的基本社交应用程序 - 状态更新(即帖子)和每个帖子的多条评论。Django ORM:在没有执行N + 1个查询的情况下检索帖子和最新评论

考虑下面的简化模型,是否有可能使用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()获取所有信息和评论,但我想检索的每帖子数量有限只要。

UPDATE:

我明白,如果这可以在所有使用Django的ORM来完成,它可能必须与某个版本的prefetch_related完成。只要我避免每页进行N + 1个查询,多个查询完全可以。

在Django中处理这个问题的典型/推荐方式是什么?

更新2:

似乎是用Django的ORM一个简单的查询,有效地做到这一点并没有直接和简便的方法。有在回答一些有用的解决方案/方法/解决方法如下,包括:

  • 缓存的最新评论的ID数据库
  • 执行原始SQL查询
  • 检索所有评论的ID,做分组和蟒蛇
  • “加入”限制您的应用程序,以显示最新评论仅

我不知道哪一个标记为正确的,因为我没有得到机会尝试所有这些方法 - 但我将hynekcer的赏金颁发给了许多选择。

更新3:

我结束了使用@ user1583799的解决方案。

+0

我不知道'.select_related( '意见')'提取意见。 '.select_related'可以获取ForeignKey的,OneToOne关系和反向OneToOne – Igor 2014-10-14 12:51:54

+0

@Igor,呵呵,我不知道是这种情况。我猜[prefetch_related]的文档(https://docs.djangoproject.com/en/1.6/ref/models/querysets/#prefetch-related)暗示这一点。感谢您的高举。 – tino 2014-10-14 16:50:41

+0

提取所有相关注释时出现什么问题?您以后可以在每篇文章中只使用前两项。 'posts [0] .comments.all()'不会执行额外的查询。这个问题是否有太多的相关查询来预取它们? – 2014-10-17 13:20:54

回答

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

+0

谢谢!如果我无法找到单独检索时执行此操作的好方法,我可能会最终跟踪数据库中最新的两条评论。我也没有意识到ArrayField,所以欣赏信息。 – tino 2014-10-20 11:16:13

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%将连续读取而不加载到内存中。


EDIT

  • 所有实施例仅用于您在棒POST_ID的条件[1,3,5]
  • 在所有情况下创建(enything早些时候按类别等选择的)关于字段注释索引[ '后', 'PK']

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 

备齐“oldest_displayed”由一个嵌套的SQL最初(和设置岗位为零不到两年的意见):

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; 
+0

谢谢,hynekcer。我不知道,但通过所有评论迭代可能不是,至少根据[这个问题](http://stackoverflow.com/questions/4222176/why-is-iterating-through-a-为您提供建议的好处,大Django的查询集消费,大规模的,大量-的-ME)。 – tino 2014-10-20 10:44:20

+0

@tino:没有。与预取相比,它读取的数据更少(相关注释的ID,没有文本),并且节省了更少的数据(只有两个最新注释的ID)。它只读取要显示的对象。我预计它比其他解决方案更快。我还不够,我可以通过缓存一个数字变量来提高速度 - 应该显示的两条评论中最早的一条主键。 – hynekcer 2014-10-20 13:40:38

+0

啊,我现在看到了内存优势,谢谢!我不得不剖析这个,看看它是否有帮助,但总体来说,缓存最后两个评论ID可能更有意义,因为在检索方面似乎没有一种简单的方法。你提到最快的解决方案是嵌套查询......你如何在Postgres后端在Django中做到这一点? – tino 2014-10-21 07:00:01

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) 

...然后如上。当然,您仍然可以对结果列表进行后处理,以将显示限制为最多两条评论。

+0

非常感谢,凯文。我不能认为评论会在特定的时间范围内,但如果我无法想出办法做到这一点,也许我只会解决单个最新的评论。(是的,新的Prefetch对象很酷 - 就在问我升级到1.7的问题之前,认为它可以做到这一点。) – tino 2014-10-20 11:03:27

相关问题