2014-10-12 53 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

相關問題