Django 从入门到精通:知行博客实战(6.1) · 第 22 / 36 节

查询优化与 Django 6.1 获取模式

吴秀峰 3 次阅读 0 条评论 ↓

学习目标

识别 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