Django 面試準備 10-4:查詢性能分析工具
掌握各種工具診斷和優化 Django 查詢性能
目錄
10-4. 查詢性能分析工具(Query Performance Tools)
📌 為什麼需要性能分析工具?
問題: 如何發現查詢性能問題?
- 🔍 發現 N+1 查詢
- 📊 識別慢查詢
- ⚡ 優化瓶頸
- 📈 監控性能
🔧 工具 1:Django Debug Toolbar
安裝配置
pip install django-debug-toolbar# settings.py
INSTALLED_APPS = [
...
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
...
]
INTERNAL_IPS = [
'127.0.0.1',
]
# 可選配置
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG,
'SQL_WARNING_THRESHOLD': 100, # 查詢時間超過 100ms 警告
}
# urls.py
from django.urls import include, path
urlpatterns = [
...
path('__debug__/', include('debug_toolbar.urls')),
]使用方法
1. 訪問網頁,點擊右側工具欄
2. 查看 SQL 面板
SQL 查詢統計:
- Total queries: 52
- Total time: 234.5ms
- Duplicate queries: 47 ⚠️ N+1 問題!
相似查詢:
SELECT * FROM author WHERE id = 1; (47 次) ⚠️3. 查看每條查詢的詳情
- SQL 語句
- 執行時間
- 調用堆棧(在哪行代碼觸發)
實用功能
檢測 N+1 查詢
# views.py
def post_list(request):
posts = Post.objects.all() # 1 次查詢
return render(request, 'posts.html', {'posts': posts})
# template.html
{% for post in posts %}
{{ post.author.name }} <!-- N 次查詢! -->
{% endfor %}Debug Toolbar 顯示:
Similar queries: 100
SELECT * FROM author WHERE id = ? (100 次)
⚠️ 建議:使用 select_related('author')查看查詢計劃
點擊「Explain」按鈕查看執行計劃:
QUERY PLAN
----------
Seq Scan on posts (cost=0.00..1500.00 rows=100000 width=100)
Filter: (status = 'published')🔧 工具 2:django-silk
安裝配置
pip install django-silk# settings.py
INSTALLED_APPS = [
...
'silk',
]
MIDDLEWARE = [
'silk.middleware.SilkyMiddleware',
...
]
# 可選配置
SILKY_PYTHON_PROFILER = True # 啟用 Python 性能分析
SILKY_PYTHON_PROFILER_BINARY = True
SILKY_MAX_REQUEST_BODY_SIZE = 1024 # 最大請求體大小(KB)
SILKY_MAX_RESPONSE_BODY_SIZE = 1024
# urls.py
urlpatterns = [
...
path('silk/', include('silk.urls', namespace='silk')),
]
# 執行遷移
python manage.py migrate使用方法
1. 訪問 /silk/ 查看所有請求
最近請求:
/api/posts/ 200 234ms 52 queries 5 分鐘前
/api/users/ 200 89ms 12 queries 10 分鐘前
/admin/ 200 1.2s 156 queries 15 分鐘前 ⚠️2. 點擊請求查看詳情
- 請求/響應
- SQL 查詢列表
- Python 性能分析
- 查詢時間分布圖
實用功能
1. 自動標記重複查詢
SQL 查詢 (52 total):
🔴 SELECT * FROM author WHERE id = ? (47 次) ⚠️ N+1
├─ Time: 235ms (total)
├─ Avg: 5ms per query
└─ Traceback: views.py:42
✅ SELECT * FROM post WHERE id = ? (1 次)
└─ Time: 3ms2. SQL 查詢聚合
按表統計:
author: 47 queries, 235ms
post: 5 queries, 45ms
comment: 12 queries, 89ms3. Python 性能分析
Function Calls Time
------------------------------------
views.post_list 1 234ms
├─ Post.objects.all() 1 3ms
├─ render() 1 231ms
└─ template render 1 228ms
└─ author.name 100 220ms ⚠️4. 導出分析報告
# 導出為 JSON
curl http://localhost:8000/silk/request/123/ > report.json
# 使用 silk_clearsilk 命令清理舊數據
python manage.py silk_clear_request_log🔧 工具 3:connection.queries
手動查看查詢
from django.db import connection
from django.test.utils import override_settings
@override_settings(DEBUG=True)
def view(request):
# 執行查詢
posts = Post.objects.all()
for post in posts:
_ = post.author.name
# 打印所有查詢
print(f"Total queries: {len(connection.queries)}")
for i, query in enumerate(connection.queries, 1):
print(f"{i}. {query['sql'][:100]}...")
print(f" Time: {query['time']}s\n")
return render(request, 'posts.html', {'posts': posts})輸出:
Total queries: 101
1. SELECT * FROM post
Time: 0.003s
2. SELECT * FROM author WHERE id = 1
Time: 0.002s
3. SELECT * FROM author WHERE id = 2
Time: 0.002s
...創建裝飾器
# utils.py
from functools import wraps
from django.db import connection, reset_queries
from django.conf import settings
def query_debugger(func):
"""裝飾器:打印查詢統計"""
@wraps(func)
def wrapper(*args, **kwargs):
if not settings.DEBUG:
return func(*args, **kwargs)
reset_queries()
result = func(*args, **kwargs)
queries = connection.queries
print(f"\n{'='*50}")
print(f"Function: {func.__name__}")
print(f"Queries: {len(queries)}")
total_time = sum(float(q['time']) for q in queries)
print(f"Total time: {total_time:.3f}s")
# 找出重複查詢
from collections import Counter
sql_counter = Counter(q['sql'] for q in queries)
duplicates = {sql: count for sql, count in sql_counter.items() if count > 1}
if duplicates:
print(f"\n⚠️ Duplicate queries:")
for sql, count in duplicates.items():
print(f" {count}x: {sql[:80]}...")
print(f"{'='*50}\n")
return result
return wrapper
# 使用
@query_debugger
def get_posts():
posts = Post.objects.all()
for post in posts:
_ = post.author.name
return posts🔧 工具 4:EXPLAIN(數據庫查詢計劃)
PostgreSQL EXPLAIN
from django.db import connection
def explain_query():
cursor = connection.cursor()
# 獲取查詢計劃
cursor.execute("EXPLAIN SELECT * FROM post WHERE status = 'published'")
plan = cursor.fetchall()
for row in plan:
print(row[0])輸出:
Seq Scan on post (cost=0.00..1500.00 rows=10000 width=100)
Filter: (status = 'published'::text)EXPLAIN ANALYZE(實際執行)
cursor.execute("""
EXPLAIN ANALYZE
SELECT * FROM post
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 10
""")
plan = cursor.fetchall()
for row in plan:
print(row[0])輸出:
Limit (cost=0.42..0.78 rows=10 width=100) (actual time=0.123..0.456 rows=10 loops=1)
-> Index Scan using post_created_at_idx on post (cost=0.42..3587.21 rows=100000 width=100) (actual time=0.122..0.453 rows=10 loops=1)
Filter: (status = 'published'::text)
Rows Removed by Filter: 5
Planning Time: 0.234 ms
Execution Time: 0.567 ms解讀 EXPLAIN 輸出
關鍵指標:
Seq Scan(全表掃描) ⚠️
Seq Scan on post (cost=0.00..1500.00 rows=100000 width=100)→ 需要添加索引
Index Scan(索引掃描) ✅
Index Scan using post_created_at_idx on post (cost=0.42..0.78 rows=10)→ 使用了索引,性能好
Index Only Scan(僅索引掃描) 🌟
Index Only Scan using post_status_idx on post→ 最優,所有數據都在索引中
Bitmap Scan(位圖掃描)
Bitmap Heap Scan on post Recheck Cond: (status = 'published'::text) -> Bitmap Index Scan on post_status_idx→ 適合返回大量數據
Django ORM 使用 EXPLAIN
# Django 2.1+
queryset = Post.objects.filter(status='published').explain()
print(queryset)
# 添加選項
queryset = Post.objects.filter(status='published').explain(
verbose=True,
analyze=True
)
print(queryset)🔧 工具 5:慢查詢日誌
PostgreSQL 慢查詢配置
# postgresql.conf
log_min_duration_statement = 100 # 記錄執行時間超過 100ms 的查詢
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d '
log_statement = 'all' # 記錄所有語句(可選)重啟 PostgreSQL:
sudo service postgresql restartDjango 慢查詢日誌
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'filters': {
'slow_query': {
'()': 'myapp.filters.SlowQueryFilter',
}
},
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': 'slow_queries.log',
'formatter': 'verbose',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['file'],
'filters': ['slow_query'],
},
},
}
# myapp/filters.py
import logging
class SlowQueryFilter(logging.Filter):
def filter(self, record):
# 只記錄執行時間超過 100ms 的查詢
if hasattr(record, 'duration'):
duration = float(record.duration)
return duration > 0.1
return False🔧 工具 6:Django 單元測試
assertNumQueries
from django.test import TestCase
from django.test.utils import override_settings
class PostViewTest(TestCase):
def setUp(self):
# 創建測試數據
author = Author.objects.create(name='Alice')
for i in range(10):
Post.objects.create(
title=f'Post {i}',
content='Content',
author=author
)
@override_settings(DEBUG=True)
def test_post_list_queries(self):
"""測試查詢次數"""
# ❌ 沒有優化:11 次查詢
with self.assertNumQueries(11):
posts = Post.objects.all()
for post in posts:
_ = post.author.name
# ✅ 使用 select_related:1 次查詢
with self.assertNumQueries(1):
posts = Post.objects.select_related('author').all()
for post in posts:
_ = post.author.name自定義斷言
from django.db import connection
from django.test import TestCase
class PerformanceTestCase(TestCase):
def assertQueryCountLessThan(self, max_queries):
"""斷言查詢次數小於指定值"""
actual = len(connection.queries)
if actual > max_queries:
self.fail(
f"Too many queries! Expected <= {max_queries}, got {actual}\n"
f"Queries: {connection.queries}"
)
def test_post_list_performance(self):
# 執行操作
posts = Post.objects.select_related('author').all()
list(posts)
# 檢查查詢次數
self.assertQueryCountLessThan(2)📊 綜合案例:性能優化流程
步驟 1:發現問題(Django Debug Toolbar)
訪問 /api/posts/
發現:52 queries, 234ms
⚠️ Similar queries: 47x SELECT * FROM author步驟 2:定位代碼(django-silk)
views.py:42
for post in posts:
print(post.author.name) ← 觸發 N+1 查詢步驟 3:優化查詢
# 修改前
posts = Post.objects.all()
# 修改後
posts = Post.objects.select_related('author').all()步驟 4:驗證效果(connection.queries)
@query_debugger
def get_posts():
return Post.objects.select_related('author').all()
# 輸出:
# Queries: 1 ✅
# Total time: 0.003s ✅步驟 5:分析執行計劃(EXPLAIN)
queryset = Post.objects.select_related('author').filter(
status='published'
).explain(analyze=True)
print(queryset)輸出:
Nested Loop (cost=0.71..28.46 rows=10 width=200) (actual time=0.023..0.145 rows=10 loops=1)
-> Index Scan using post_status_idx on post (cost=0.42..12.89 rows=10) ✅
-> Index Scan using author_pkey on author (cost=0.29..1.56 rows=1) ✅
Planning Time: 0.234 ms
Execution Time: 0.189 ms ✅步驟 6:添加單元測試
def test_post_list_queries(self):
"""確保查詢次數不增加"""
with self.assertNumQueries(1):
posts = Post.objects.select_related('author').all()
list(posts)💡 工具選擇指南
| 工具 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| Debug Toolbar | 開發環境,快速調試 | 即時可視化,易用 | 只能看當前請求 |
| django-silk | 性能分析,歷史記錄 | 記錄所有請求,Python profiling | 有性能開銷 |
| connection.queries | 代碼調試,單元測試 | 簡單直接,無依賴 | 需要手動打印 |
| EXPLAIN | SQL 優化,索引分析 | 最詳細的執行計劃 | 需要理解 SQL |
| 慢查詢日誌 | 生產環境監控 | 持續監控 | 需要配置數據庫 |
💡 最佳實踐
1. 開發環境必備工具
# settings/dev.py
INSTALLED_APPS += [
'debug_toolbar',
'silk',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'silk.middleware.SilkyMiddleware',
] + MIDDLEWARE2. 單元測試覆蓋
class ViewTestCase(TestCase):
def test_all_views_query_count(self):
"""測試所有視圖的查詢次數"""
test_cases = [
('/api/posts/', 2), # 期望 2 次查詢
('/api/users/', 1), # 期望 1 次查詢
('/api/comments/', 3), # 期望 3 次查詢
]
for url, max_queries in test_cases:
with self.subTest(url=url):
with self.assertNumQueries(max_queries):
self.client.get(url)3. 持續監控
# middleware.py
from django.db import connection
class QueryCountMiddleware:
"""查詢次數監控中間件"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 重置查詢計數
connection.queries_log.clear()
response = self.get_response(request)
# 記錄查詢統計
query_count = len(connection.queries)
if query_count > 50: # 超過 50 次查詢發出警告
logger.warning(
f"Too many queries: {query_count} for {request.path}"
)
# 添加響應頭
response['X-DB-Query-Count'] = query_count
return response💡 面試要點
Q1: 如何發現 Django 應用的性能問題?
答:
- Django Debug Toolbar:開發環境即時查看
- django-silk:記錄和分析所有請求
- 慢查詢日誌:生產環境監控
- 單元測試:assertNumQueries 防止退化
Q2: EXPLAIN 的主要輸出類型有哪些?
答:
- Seq Scan:全表掃描(慢)
- Index Scan:使用索引(快)
- Index Only Scan:只訪問索引(最快)
- Bitmap Scan:位圖掃描(適合大量數據)
Q3: 生產環境如何監控查詢性能?
答:
- 數據庫慢查詢日誌
- APM 工具(New Relic, Datadog)
- 自定義中間件記錄查詢統計
- 定期審查 django-silk 數據
Q4: Django Debug Toolbar 會影響性能嗎?
答:
- 會,因為要收集和顯示大量信息
- 只應在開發環境使用
- 生產環境必須禁用(
DEBUG = False)
🎓 總結
性能優化三步走:
測量
- 使用工具發現問題
- 不要憑感覺優化
分析
- N+1 查詢?→ select_related/prefetch_related
- 慢查詢?→ EXPLAIN 分析執行計劃
- 缺索引?→ 添加合適的索引
驗證
- 再次測量確認優化效果
- 添加單元測試防止退化
恭喜!你已完成第 10 章「SQL 查詢優化」的所有內容!
系列文章:
下一章: 11. 線程安全