ORM 的陷阱¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解什麼是 N+1 查詢問題
- 使用
runserver_plus --print-sql觀察 SQL 查詢 - 識別並解決 ForeignKey 的 N+1 問題
- 識別並解決 ManyToManyField 的 N+1 問題
- 學習使用
select_related和prefetch_related - 掌握 ORM 效能優化的最佳實踐
什麼是 N+1 查詢問題?¶
N+1 查詢問題是使用 ORM 時最常見的效能問題之一。
問題場景¶
假設你要顯示 10 篇文章及其作者名稱:
# 取得所有文章
articles = Article.objects.all() # 1 次查詢
# 在 template 中顯示
for article in articles:
print(article.author.name) # 每篇文章都會查詢 1 次作者!
查詢次數爆炸
- 取得文章列表:1 次查詢
- 取得每篇文章的作者:N 次查詢(N = 文章數量)
- 總共:1 + N 次查詢
如果有 100 篇文章,就會執行 101 次資料庫查詢!
為什麼會發生?¶
Django ORM 採用「延遲載入」(Lazy Loading)策略:
- 查詢
Article.objects.all()時,只載入文章資料 - 當存取
article.author時,才會再查詢資料庫取得作者資料 - 每次存取都會觸發一次資料庫查詢
這個設計在單一物件時很有效率,但在處理列表時就會造成效能問題。
觀察 N+1 問題¶
讓我們實際觀察文章列表頁面執行了多少 SQL 查詢。
啟動 runserver_plus 並監控 SQL¶
使用 --print-sql 參數啟動開發伺服器:
回顧 Django Extensions
runserver_plus --print-sql 是 Django Extensions 提供的功能。
如果還沒安裝,請參考前一章節「Django Extensions」。
訪問文章列表頁面¶
在瀏覽器中開啟:http://127.0.0.1:8000/blog/articles/
在終端機中,你會看到類似這樣的 SQL 查詢輸出:
SELECT "blog_article"."id",
"blog_article"."title",
"blog_article"."content",
"blog_article"."author_id",
"blog_article"."created_at",
"blog_article"."updated_at"
FROM "blog_article"
Execution time: 0.001s [Database: default]
SELECT "blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio"
FROM "blog_author"
WHERE "blog_author"."id" = 1
Execution time: 0.001s [Database: default]
SELECT "blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio"
FROM "blog_author"
WHERE "blog_author"."id" = 2
Execution time: 0.001s [Database: default]
SELECT "blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio"
FROM "blog_author"
WHERE "blog_author"."id" = 1
Execution time: 0.001s [Database: default]
... 更多類似的查詢 ...
發現問題了嗎?
仔細觀察這些 SQL 查詢:
- 第一個查詢:取得所有文章
- 後續查詢:每篇文章都查詢一次作者資料
如果有 10 篇文章,就會執行 11 次查詢!
這就是典型的 N+1 查詢問題。
為什麼會這樣?¶
回顧 blog/templates/blog/components/article_card.html:
當 template 渲染時:
for article in articles迴圈遍歷每篇文章- 每次存取
article.author.name時,Django 發現作者資料還沒載入 - Django 立即查詢資料庫取得該作者的資料
- 重複 N 次(N = 文章數量)
解決方案:select_related¶
select_related 可以解決 ForeignKey 和 OneToOneField 的 N+1 問題。
什麼是 select_related?¶
select_related 使用 SQL 的 JOIN 語法,在一次查詢中就取得關聯資料。
# ❌ 會產生 N+1 問題
articles = Article.objects.all()
# ✅ 使用 select_related,只執行一次查詢
articles = Article.objects.select_related('author').all()
修改 article_list View¶
修改 blog/views.py:
from django.shortcuts import get_object_or_404, render
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").all()
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(Article, id=article_id)
return render(request, "blog/article_detail.html", {"article": article})
只需要在原本的查詢加上 .select_related("author")!
select_related 參數
select_related 的參數是要載入的關聯欄位名稱:
select_related("author"):載入 author 關聯select_related("author", "category"):載入多個關聯select_related("author__country"):載入巢狀關聯(作者的國家)
再次觀察 SQL 查詢¶
重新整理瀏覽器頁面,觀察終端機的 SQL 輸出:
SELECT "blog_article"."id",
"blog_article"."title",
"blog_article"."content",
"blog_article"."author_id",
"blog_article"."created_at",
"blog_article"."updated_at",
"blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio"
FROM "blog_article"
LEFT OUTER JOIN "blog_author"
ON ("blog_article"."author_id" = "blog_author"."id")
Execution time: 0.002s [Database: default]
問題解決!
現在只執行了 1 次查詢!
使用 LEFT OUTER JOIN 將文章和作者的資料一次取回。
無論有多少篇文章,都只需要 1 次查詢。
效能比較¶
| 方法 | 文章數量 | SQL 查詢次數 | 執行時間 |
|---|---|---|---|
| 未優化 | 10 | 11 次 | ~10ms |
| select_related | 10 | 1 次 | ~2ms |
| 未優化 | 100 | 101 次 | ~100ms |
| select_related | 100 | 1 次 | ~3ms |
效能差異
隨著資料量增加,效能差異會更加明顯:
- 減少資料庫查詢次數
- 降低網路往返時間
- 提升頁面載入速度
在生產環境中,這可能是 100ms 和 10ms 的差異!
在文章列表顯示標籤¶
現在讓我們在文章卡片中顯示標籤資訊。
修改 article_card.html¶
修改 blog/templates/blog/components/article_card.html:
<div class="col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3 mb-4">
<div class="card h-100">
<div class="card-body d-flex flex-column">
<h5 class="card-title">
{{ article.title }}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
{{ article.author.name }}
</h6>
{% if article.tags.exists %}
<div class="mb-2">
{% for tag in article.tags.all %}
<span class="badge bg-secondary me-1">{{ tag.name }}</span>
{% endfor %}
</div>
{% endif %}
<p class="card-text flex-grow-1">
{{ article.content|truncatewords:30 }}
</p>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ article.created_at|date:"Y-m-d" }}
</small>
<a href="{% url 'blog:article_detail' article.id %}"
class="btn btn-primary btn-sm">
閱讀更多
</a>
</div>
</div>
</div>
</div>
我們新增了一個區塊來顯示文章的標籤。
ManyToManyField 的 N+1 問題¶
觀察 SQL 查詢¶
重新整理頁面,觀察終端機的 SQL 輸出:
SELECT "blog_article"."id",
"blog_article"."title",
"blog_article"."content",
"blog_article"."created_at",
"blog_article"."updated_at",
"blog_article"."is_published",
"blog_article"."author_id",
"blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio",
"blog_author"."created_at"
FROM "blog_article"
LEFT OUTER JOIN "blog_author"
ON ("blog_article"."author_id" = "blog_author"."id")
Execution time: 0.000100s [Database: default]
SELECT 1 AS "a" -- (1)!
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 2
LIMIT 1
Execution time: 0.000108s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" = 2
Execution time: 0.000140s [Database: default]
SELECT 1 AS "a"
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 3
LIMIT 1
Execution time: 0.000068s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" = 3
Execution time: 0.000099s [Database: default]
SELECT 1 AS "a"
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 4
LIMIT 1
Execution time: 0.000143s [Database: default]
SELECT 1 AS "a"
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 5
LIMIT 1
Execution time: 0.000070s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" = 5
Execution time: 0.000069s [Database: default]
SELECT 1 AS "a"
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 6
LIMIT 1
Execution time: 0.000093s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" = 6
Execution time: 0.000083s [Database: default]
... 更多類似的查詢 ...
- 因為我們使用
.exists所以會產生這個類型的 Query
又出現 N+1 問題了!
這次是標籤的 N+1 問題:
- 第一個查詢:取得所有文章(包含作者,已優化)
- 後續查詢:每篇文章都需要確認標籤資料是否存在,若存在再查詢一次標籤資料
如果有 10 篇文章,就會執行 1 + 10 = 11 次查詢(先忽略確認是否存在的那次,計算上來只會更多)。
為什麼 select_related 不能解決?¶
select_related 的限制
select_related 只能用於:
- ForeignKey(多對一關聯)
- OneToOneField(一對一關聯)
不能用於 ManyToManyField(多對多關聯)!
為什麼?因為:
- ForeignKey 使用簡單的 JOIN 就能取得資料
- ManyToManyField 需要透過中介表,無法用簡單的 JOIN 一次取得所有資料
解決方案:prefetch_related¶
prefetch_related 可以解決 ManyToManyField 和反向 ForeignKey 的 N+1 問題。
什麼是 prefetch_related?¶
prefetch_related 使用額外的 SQL 查詢,但會批次載入所有關聯資料。
# ❌ 會產生 N+1 問題
articles = Article.objects.all()
# ✅ 使用 prefetch_related
articles = Article.objects.prefetch_related('tags').all()
select_related vs prefetch_related¶
| 特性 | select_related | prefetch_related |
|---|---|---|
| 適用關聯 | ForeignKey, OneToOneField | ManyToManyField, 反向 ForeignKey |
| 查詢方式 | 使用 SQL JOIN,一次查詢 | 分開查詢,然後在 Python 中組合 |
| 查詢次數 | 1 次 | 2 次(但是批次查詢) |
| 適用場景 | 一對一、多對一 | 一對多、多對多 |
修改 article_list View¶
修改 blog/views.py,同時使用 select_related 和 prefetch_related:
from django.shortcuts import get_object_or_404, render
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags") # (1)!
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(Article, id=article_id)
return render(request, "blog/article_detail.html", {"article": article})
- 在
objects後有跟其他查詢的情況下.all()是可以被省略的
我們在原本的查詢後面加上 .prefetch_related("tags")。
鏈式呼叫
Django ORM 支援鏈式呼叫,可以組合多個方法:
Article.objects.select_related("author").prefetch_related("tags").filter(is_published=True).order_by("-created_at")
每個方法都會回傳一個新的 QuerySet,可以繼續串接其他方法。
再次觀察 SQL 查詢¶
重新整理頁面,觀察 SQL 輸出:
SELECT "blog_article"."id",
"blog_article"."title",
...
FROM "blog_article"
LEFT OUTER JOIN "blog_author"
ON ("blog_article"."author_id" = "blog_author"."id")
Execution time: 0.002s [Database: default]
SELECT ("blog_article_tags"."article_id") AS "_prefetch_related_val_article_id",
"blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Execution time: 0.001s [Database: default]
標籤問題也解決了!
現在總共只執行了 2 次查詢:
- 第一次查詢:取得所有文章和作者(使用 JOIN)
- 第二次查詢:批次取得所有文章的標籤(使用
IN查詢)
注意第二個查詢使用了 WHERE article_id IN (1, 2, 3, ...),一次取得所有文章的標籤!
效能比較¶
| 優化階段 | 查詢次數(10 篇文章) | 說明 |
|---|---|---|
| 未優化 | 21 次 | 1 (文章) + 10 (作者) + 10 (標籤) |
| select_related | 11 次 | 1 (文章+作者) + 10 (標籤) |
| 完整優化 | 2 次 | 1 (文章+作者) + 1 (批次標籤) |
查詢次數固定
使用 prefetch_related 後,無論有多少篇文章:
- 未優化:1 + N + N = 1 + 2N 次查詢
- 完整優化:2 次查詢(固定)
當 N = 100 時,差異是 201 次 vs 2 次!
檢查文章詳情頁面¶
現在讓我們檢查文章詳情頁面是否也有類似的問題。
觀察 article_detail 頁面¶
訪問任一文章的詳情頁面,例如:http://127.0.0.1:8000/blog/articles/1/
觀察終端機的 SQL 輸出:
SELECT "blog_article"."id",
"blog_article"."title",
"blog_article"."content",
"blog_article"."created_at",
"blog_article"."updated_at",
"blog_article"."is_published",
"blog_article"."author_id"
FROM "blog_article"
WHERE "blog_article"."id" = 3
LIMIT 21
Execution time: 0.000163s [Database: default]
SELECT "blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio",
"blog_author"."created_at"
FROM "blog_author"
WHERE "blog_author"."id" = 1
LIMIT 21
Execution time: 0.000250s [Database: default]
SELECT 1 AS "a"
FROM "blog_article_tags"
WHERE "blog_article_tags"."article_id" = 3
LIMIT 1
Execution time: 0.000075s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" = 3
Execution time: 0.000117s [Database: default]
詳情頁面也有問題
雖然只顯示一篇文章,但仍然執行了 4 次查詢:
- 查詢文章
- 查詢作者
- 確認是否有 tags
- 查詢標籤
單一文章時問題不大,但如果頁面需要顯示多個相關文章,問題就會浮現。 而且多次訪問資料庫也會產生一定的網路資源浪費。
優化 article_detail View¶
修改 blog/views.py:
from django.shortcuts import get_object_or_404, render
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
get_object_or_404 中使用優化
get_object_or_404 的第一個參數可以是:
- Model:
get_object_or_404(Article, id=1) - QuerySet:
get_object_or_404(Article.objects.select_related("author"), id=1)
這讓我們可以在取得物件時就進行優化!
再次觀察 SQL 查詢¶
重新整理詳情頁面,觀察 SQL 輸出:
SELECT "blog_article"."id",
"blog_article"."title",
"blog_article"."content",
"blog_article"."author_id",
"blog_article"."created_at",
"blog_article"."updated_at",
"blog_author"."id",
"blog_author"."name",
"blog_author"."email",
"blog_author"."bio"
FROM "blog_article"
LEFT OUTER JOIN "blog_author"
ON ("blog_article"."author_id" = "blog_author"."id")
WHERE "blog_article"."id" = 1
Execution time: 0.001s [Database: default]
SELECT ("blog_article_tags"."article_id") AS "_prefetch_related_val_article_id",
"blog_tag"."id",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_article_tags"
ON ("blog_tag"."id" = "blog_article_tags"."tag_id")
WHERE "blog_article_tags"."article_id" IN (1)
Execution time: 0.001s [Database: default]
優化完成
現在只需要 2 次查詢:
- 取得文章和作者(使用 JOIN)
- 取得標籤
即使只有一篇文章,預先載入關聯資料也是好習慣,能確保效能一致。
最佳實踐¶
1. 永遠思考查詢效能¶
開發時的好習慣
在開發時使用 runserver_plus --print-sql:
- 即時看到每個頁面執行的 SQL 查詢
- 發現 N+1 問題時立即優化
- 養成效能意識
2. 根據關聯類型選擇方法¶
| 關聯類型 | 使用方法 | 範例 |
|---|---|---|
| ForeignKey | select_related |
select_related("author") |
| OneToOneField | select_related |
select_related("profile") |
| ManyToManyField | prefetch_related |
prefetch_related("tags") |
| 反向 ForeignKey | prefetch_related |
prefetch_related("comments") |
常見錯誤
3. 組合使用¶
一個查詢可以同時使用兩種方法:
# ✅ 同時優化多種關聯
Article.objects.select_related(
"author", # ForeignKey
"category", # ForeignKey
).prefetch_related(
"tags", # ManyToManyField
"comments", # 反向 ForeignKey
)
4. 巢狀關聯¶
可以使用雙底線存取巢狀關聯:
# 載入文章、作者、以及作者的國家
Article.objects.select_related("author__country")
# 載入文章、標籤、以及每個標籤的分類
Article.objects.prefetch_related("tags__category")
5. 何時不需要優化?¶
不是所有情況都需要優化
以下情況可以不用優化:
- 只顯示單一物件,且不存取關聯資料
- 關聯資料不會被使用(例如只顯示文章標題,不顯示作者)
但是過度優化也可能造成不必要的記憶體使用。
除錯工具¶
Django Debug Toolbar¶
除了 --print-sql,還可以使用 Django Debug Toolbar。
這是一個強大的除錯工具,可以在瀏覽器中直接顯示每個請求的詳細資訊。
安裝¶
使用 uv 安裝(加入 dev group,因為只在開發環境使用):
設定¶
步驟 1:在 INSTALLED_APPS 最後條件式加入
INSTALLED_APPS = [
# Django 內建 apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 第三方 apps
"django_bootstrap5",
"django_extensions",
# 本地 apps
"practices",
"blog",
]
if DEBUG: # (1)!
INSTALLED_APPS += ["debug_toolbar"]
- 只在 DEBUG 模式下加入 Debug Toolbar
為什麼要條件式加入?
因為 django-debug-toolbar 是安裝在 dev group 中,只在開發環境需要。
使用 if DEBUG: 條件式加入可以確保:
- 開發環境(
DEBUG = True):自動啟用 Debug Toolbar - 正式環境(
DEBUG = False):不會載入 Debug Toolbar
這是 Django 的最佳實踐!
步驟 2:在 MIDDLEWARE 最後條件式加入
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if DEBUG: # (1)!
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE]
- 只在 DEBUG 模式下加入 Debug Toolbar Middleware
Middleware 順序很重要
Debug Toolbar 的 middleware 要盡可能地放在 middleware 列表的前面。
這樣可以確保它能捕捉到所有其他 middleware 的處理過程。
詳細說明請參考文件
步驟 3:設定 INTERNAL_IPS
INTERNAL_IPS 的作用
INTERNAL_IPS 定義哪些 IP 位址可以看到 Debug Toolbar。
127.0.0.1是本地開發環境的 IP- 只有從這個 IP 發出的請求才會顯示 Debug Toolbar
- 這是一個安全措施,避免在開發伺服器被外部存取時洩漏資訊
P.S. 這個設定不是 Debug Toolbar 提供的,而是 Django 內建的可以參考文件
修改 core/urls.py,加入 Debug Toolbar 的 URL 路由:
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
條件式載入
使用 if settings.DEBUG: 確保 Debug Toolbar 只在開發環境啟用。
正式環境中(DEBUG = False),這些 URL 不會被載入。
使用¶
啟動開發伺服器:
訪問任何頁面(例如文章列表),你會在頁面右側看到一個工具列:
- SQL:顯示所有執行的 SQL 查詢
- 可以看到每個查詢的執行時間
- 會標示重複的查詢
- 點擊可以看到完整的 SQL 語句和執行堆疊
- Time:頁面渲染時間分析
- Templates:使用的 template 列表
- Static Files:載入的靜態檔案
- Headers:HTTP 標頭資訊
- Request:請求詳細資訊
Debug Toolbar 的優勢
相比 --print-sql,Debug Toolbar 提供:
- 視覺化介面:更容易閱讀和分析
- 重複查詢檢測:自動標示重複的查詢
- 查詢時間分析:快速找出慢查詢
- 完整的執行堆疊:知道查詢是從哪裡觸發的
- 不需要看終端機:直接在瀏覽器中檢視
兩者可以搭配使用:
- 開發時用 Debug Toolbar 視覺化分析
- 需要完整 SQL 輸出時用
--print-sql
使用 Django Shell 測試¶
在優化前後使用 shell 測試查詢:
# 未優化
>>> articles = Article.objects.all()
>>> for article in articles:
... print(article.author.name)
# 觀察執行了多少次查詢
# 優化後
>>> articles = Article.objects.select_related("author").all()
>>> for article in articles:
... print(article.author.name)
# 應該只有一次查詢
常見問題¶
select_related 和 prefetch_related 可以一起用嗎?¶
可以!而且經常需要一起使用:
這樣可以同時優化不同類型的關聯。
會不會載入太多不需要的資料?¶
使用 select_related 和 prefetch_related 確實會載入更多資料到記憶體,但:
利大於弊
- 減少資料庫查詢次數遠比多載入一些資料重要
- 資料庫往返時間通常是效能瓶頸
- 記憶體使用增加通常可以接受
- 可以用
.only()和.defer()進一步控制載入的欄位
prefetch_related 為什麼不用 JOIN?¶
因為 ManyToManyField 的特性:
- 使用中介表連接
- 一篇文章可能有多個標籤
- 一個標籤可能屬於多篇文章
如果使用 JOIN,結果會產生笛卡爾積(Cartesian Product),造成:
- 大量重複資料
- 記憶體浪費
- 查詢效能下降
prefetch_related 使用分開查詢的方式,然後在 Python 中組合,效能更好。
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解什麼是 N+1 查詢問題
- 使用
runserver_plus --print-sql觀察 SQL 查詢 - 識別並解決 ForeignKey 的 N+1 問題
- 識別並解決 ManyToManyField 的 N+1 問題
- 學習使用
select_related和prefetch_related - 掌握 ORM 效能優化的最佳實踐
效能優化的黃金法則
-
永遠使用
--print-sql檢查查詢- 在開發時就發現問題
- 不要等到上線才發現效能問題
-
記住優化方法
- ForeignKey / OneToOneField →
select_related - ManyToManyField / 反向 ForeignKey →
prefetch_related
- ForeignKey / OneToOneField →
-
養成好習慣
- 在撰寫 view 時就考慮優化
- 列表頁面一定要優化
- 即使單一物件也建議優化
-
不要過度優化
- 只優化會被使用的關聯
- 測量實際效能影響
- 在可維護性和效能間取得平衡
掌握這些技巧後,你的 Django 應用效能會大幅提升!
記住:效能問題通常出在資料庫查詢,優化 ORM 查詢是提升 Django 應用效能的關鍵。