為什麼Django ORM中的N+1問題如此常被提及?

使用Django ORM時,您會經常聽到「N+1問題」這個詞。
但如果沒有親自體驗,很難感受到這個問題有多嚴重。

簡而言之,「本應只執行一次的查詢卻意外執行了過多次」
結果,頁面加載速度會顯著變慢,且隨著數據增多,性能下降會變得更加嚴重。
特別是在查詢模型之間有關聯的數據時,這種情況經常會出現,所以使用ORM時必須了解它。

💡 N+1問題,讓我們輕鬆理解一下

假設我們在建立一個博客系統。
每個作者(Author)可以撰寫多篇文章(Post)
用Django模型表示如下:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

每個作者(Author)可以撰寫多篇文章(Post),因此是1:N關係

現在假設我們要實現一個功能,輸出所有作者的名字和他所寫的文章標題
一般來說,您可能會這樣編寫:

⚠️ 產生N+1問題的代碼

authors = Author.objects.all()  # (1) 第一次查詢 (查詢Author表)

for author in authors:
    print(author.post_set.all())  # (N) 各Author的Post單獨查詢

執行一次後,加載時間可能會比預期的長。
如果數據不多,可能不會明顯,但如果有100位作者和超過500篇文章呢?
問題隨之而來,漸漸顯露出來。

🧐 實際執行的SQL查詢

SELECT * FROM author;  -- 執行1次
SELECT * FROM post WHERE author_id = 1;  -- 執行N次
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...

隨著作者數量的增加,查詢次數呈指數增長。
這就是N+1問題

🚀 N+1問題在哪些情況下經常發生?

事實上,無論是否善用Django ORM,
查詢兩個或更多相關模型的數據時,這個問題幾乎總是必須注意的
特別是在以下情況下經常發生:

1️⃣ 在模板中循環引用數據的情況

{% for author in authors %}
    

{{ author.name }}

    {% for post in author.post_set.all %}
  • {{ post.title }}
  • {% endfor %}
{% endfor %}

在模板中循環訪問數據時,
每個對象都會產生附加的查詢,從而導致性能下降。

2️⃣ 在視圖函數中循環內部引用相關模型

def blog_view(request):
    authors = Author.objects.all()  # 第一次查詢 (查詢Author)
    
    author_list = []
    for author in authors:
        author_list.append({
            'name': author.name,
            'posts': author.post_set.all()  # 產生N個附加查詢
        })
    
    return render(request, 'blog.html', {'authors': author_list})

在視圖函數中處理數據時也會出現同樣的問題。

💡 我親身經歷的N+1問題

我曾經製作過一個名為get_absolute_url()的自定義方法。
這是一個通過組合模型的多個字段生成URL的函數,
在性能變慢的問題出現後我才找到了原因。

class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

    def get_absolute_url(self):
        return f"/author/{self.author.name}/{self.slug}/"

每次調用此方法時,self.author.name會執行,
並且產生N個附加查詢
為了解決這個問題,我花了不少時間。

✅ 總結:必須理解N+1問題的原因

  • 只要使用Django ORM,這個問題隨時可能發生。
  • 如果頁面速度變慢,ORM執行了比預期多的查詢的可能性很高。
  • 在模板、視圖函數、模型方法中都可能會發生。
  • 數據越多,性能下降就會越嚴重。
  • ORM不會自動優化,因此需要手動檢查執行的查詢並進行優化。

在下一篇文章中,我將介紹如何使用select_relatedprefetch_related來解決N+1問題。

Django ORM中的N+1查詢問題