查询优化与 Django 6.1 获取模式
学习目标
识别 N+1 查询,使用 select_related、prefetch_related 和 6.1 fetch_mode 的适用场景,用查询数验证而不是凭感觉优化。
为什么循环可能偷偷执行 SQL
读取三篇文章是一条 SQL;如果每次访问 post.author 时再查用户,合计就是四条,这类模式称为 N+1。访问关联属性看起来和普通字段一样,实际可能触发数据库请求。模板循环同样会发生这个问题。
已知需要的外键适合 select_related,它用 JOIN 获取单值关系。多对多或反向一对多适合 prefetch_related,通常执行额外查询后在 Python 中组合。本项目列表已经明确需要作者与分类,因此保留显式加载,让查询意图清晰。
6.1 新增获取模式:FETCH_ONE 保持按对象补取,FETCH_PEERS 为同一查询集中的同类缺失字段批量获取,FETCH_RAISE 阻止意外补取并抛出 FieldFetchBlocked。它不自动优化所有反向管理器查询,也不意味着可以忽略查询设计。
具体怎么做
新增测试文件,分别验证 JOIN、按需批量获取和禁止隐式获取。异常类从 django.core.exceptions 导入,不要凭常量的位置猜测异常导出路径。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
blog/test_queries.py
from django.contrib.auth import get_user_model
from django.db import models
from django.core.exceptions import FieldFetchBlocked
from django.test import TestCase
from .models import Post, Category
class QueryTests(TestCase):
@classmethod
def setUpTestData(cls):
category = Category.objects.create(name="优化", slug="performance")
for number in range(3):
author = get_user_model().objects.create_user(f"author{number}", f"a{number}@example.com")
Post.objects.create(title=f"查询演示{number}", body="内容", author=author, category=category)
def test_explicit_join(self):
with self.assertNumQueries(1):
names = [post.author.username for post in Post.objects.select_related("author")]
self.assertEqual(len(names), 3)
def test_peer_fetch(self):
with self.assertNumQueries(2):
names = [post.author.username for post in Post.objects.fetch_mode(models.FETCH_PEERS)]
self.assertEqual(len(names), 3)
def test_unexpected_fetch_raises(self):
post = Post.objects.fetch_mode(models.FETCH_RAISE).first()
with self.assertRaises(FieldFetchBlocked):
_ = post.author.username
如何确认成功
python manage.py test blog.test_queries --settings=config.test_settings
三项测试通过。显式 JOIN 读取三个作者只执行 1 条查询;FETCH_PEERS 执行 2 条;FETCH_RAISE 在访问未加载 author 时抛出预期异常。
接着在 shell 中查看列表查询的执行计划:
from blog.models import Post
query = Post.objects.filter(status="published").order_by("-created_at", "-pk")
print(query.explain())
不同数据库输出不同,应关注是否使用合适索引、扫描量和排序成本,而不是要求计划文本完全一致。数据很小时优化器可能认为全表扫描更便宜。
练习:故意删去 select_related,测量遍历 3 篇文章时的查询数;恢复后对比。考虑深页 offset 扫描成本:按 (created_at, pk) 游标翻页可以避免跳过大量记录,但无法直接跳任意页,应根据产品需求选择。
常见问题
only/defer 一定更快吗? 若模板后来读取被延迟字段,会额外发 SQL,可能更慢。每条 SQL 都要优化到一条吗? 不一定,大 JOIN 可能传输重复数据,少量可预期查询更容易控制。开启 DEBUG 计时可靠吗? 它会影响性能,正式压测应使用生产设置、真实数据规模并观察 p95 和错误率。
官方参考
登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。
登录并保存学习进度 →联系站长
微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。
章节讨论 0