CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Django 实战入门/课时/查询与优化
RawBlame
课时目录12 节

Django 实战入门

  1. 01Django 概览
  2. 02项目结构
  3. 03URL 路由
  4. 04视图层
  5. 05模型与 ORM
  6. 06查询与优化
  7. 07模板 DTL
  8. 08Admin 与认证
  9. 09表单与安全
  10. 10DRF 与异步任务
  11. 11测试与部署
  12. 12综合练习

查询与优化

QuerySet、聚合与 lookup。

30 分钟

查询与优化

这一课你将能做到

  1. 用 filter/exclude/annotate 写出等价于复杂 SQL 的 QuerySet 查询
  2. 用 aggregate + F/Q 表达式完成统计分析和条件组合查询
  3. 用 select_related / prefetch_related 根治 N+1 问题,并用 connection.queries 验证效果

为什么要学

ORM 写得爽,但写得不对就是性能杀手。90% 的 Django 性能问题都出在查询层:N+1 查询、没加索引、一次拉全表。学会正确用 QuerySet API 是 Django 工程师和初学者的分水岭。

和 Spring Boot 对比:Spring Data JPA 用 @Query 注解或方法名约定(findByStatusAndCreatedAtAfter),Django 用链式调用 filter().order_by()。两者最终都生成 SQL,但 Django 的查询 API 更灵活、组合性更强。

先建立图像

Python 链式调用                          生成的 SQL
────────────────────────────────────  ─────────────────────────────
Article.objects                         SELECT * FROM article
  .filter(status="published")           WHERE status = 'published'
  .exclude(author__username="admin")    AND author.username != 'admin'
  .order_by("-created_at")              ORDER BY created_at DESC
  [:10]                                 LIMIT 10

跟着做

步骤 1:双下划线 lookup 速查

Django 用双下划线 __ 表示字段查询操作符:

from blog.models import Article
from django.contrib.auth.models import User

# 等于(默认)
Article.objects.filter(status="published")

# 不等于
Article.objects.exclude(status="draft")

# 大于/小于
Article.objects.filter(views__gt=100)         # views > 100
Article.objects.filter(views__gte=100)        # views >= 100
Article.objects.filter(views__lt=1000)        # views < 1000

# 范围
Article.objects.filter(views__range=(100, 1000))   # BETWEEN 100 AND 1000

# 模糊匹配
Article.objects.filter(title__contains="Django")   # LIKE '%Django%'(区分大小写)
Article.objects.filter(title__icontains="django")  # LIKE '%django%'(不区分)
Article.objects.filter(title__startswith="Python")
Article.objects.filter(title__endswith="教程")

# IN 查询
Article.objects.filter(id__in=[1, 2, 3, 4])

# 日期查询
Article.objects.filter(created_at__year=2024)
Article.objects.filter(created_at__month=8)
Article.objects.filter(created_at__date="2024-08-01")

# 关联查询(跨表用双下划线)
Article.objects.filter(author__username="alice")           # JOIN auth_user
Article.objects.filter(category__name="Python")            # JOIN category
Article.objects.filter(tags__name__icontains="django")     # JOIN 中间表 + tag

# NULL 判断
Article.objects.filter(category__isnull=True)

# 正则匹配
Article.objects.filter(slug__regex=r"^django-\d+")

步骤 2:链式调用与惰性求值

QuerySet 是「惰性」的,直到你真正迭代/切片/取长度时才执行 SQL:

qs = Article.objects.filter(status="published")  # 还没执行 SQL
qs = qs.order_by("-created_at")                  # 还没执行
qs = qs[:10]                                     # 还没执行
for a in qs:                                     # 此刻才执行 SELECT
    print(a.title)

这意味着可以动态构建查询:

def search_articles(keyword=None, category_id=None, min_views=0):
    qs = Article.objects.filter(status="published")
    if keyword:
        qs = qs.filter(title__icontains=keyword)
    if category_id:
        qs = qs.filter(category_id=category_id)
    if min_views > 0:
        qs = qs.filter(views__gte=min_views)
    return qs.order_by("-created_at")

步骤 3:Q 对象组合复杂条件

| 表示 OR,& 表示 AND,~ 表示 NOT:

from django.db.models import Q

# (status='published' AND views>100) OR title contains 'Django'
Article.objects.filter(
    Q(status="published", views__gt=100) | Q(title__icontains="Django")
)

# 复杂嵌套
Article.objects.filter(
    Q(status="published") &
    (Q(author__username="alice") | Q(author__username="bob")) &
    ~Q(category__name="广告")
)

步骤 4:F 表达式和批量更新

F 表达式引用「数据库字段本身的值」,避免 Python 层先取出再写回:

from django.db.models import F

# 错误:先查再改,两次 SQL,且有并发问题
article = Article.objects.get(id=1)
article.views += 1
article.save()

# 正确:一条 UPDATE 语句,原子操作
Article.objects.filter(id=1).update(views=F("views") + 1)

# 批量操作
Article.objects.filter(status="draft").update(
    views=F("views") * 2,
    updated_at=timezone.now()
)

步骤 5:聚合 annotate 和 aggregate

from django.db.models import Count, Avg, Max, Sum, Q

# aggregate:返回单个统计结果(字典)
stats = Article.objects.aggregate(
    total=Count("id"),
    avg_views=Avg("views"),
    max_views=Max("views"),
    total_views=Sum("views")
)
# {'total': 100, 'avg_views': 523.4, ...}

# annotate:给每个对象附加统计字段
from django.contrib.auth.models import User

# 每个作者的文章数
authors = User.objects.annotate(article_count=Count("articles"))
for author in authors:
    print(author.username, author.article_count)

# 每个分类的已发布文章数(条件聚合)
from blog.models import Category
cats = Category.objects.annotate(
    published_count=Count("articles", filter=Q(articles__status="published"))
).order_by("-published_count")

步骤 6:select_related 与 prefetch_related

N+1 演示(糟糕的代码):

# 假设有 100 篇文章
articles = Article.objects.all()  # 1 次查询
for a in articles:
    print(a.author.username)      # 每次循环都查一次 author,共 100 次
    print(a.category.name)        # 再 100 次
    print([t.name for t in a.tags.all()])  # 再 100 次
# 总计:1 + 100 + 100 + 100 = 301 次 SQL!

优化后:

articles = (
    Article.objects
    .select_related("author", "category")  # FK 用 select_related(SQL JOIN)
    .prefetch_related("tags")              # M2M 用 prefetch_related(2 次查询)
    .all()
)
for a in articles:
    print(a.author.username)      # 不再触发 SQL
    print(a.category.name)
    print([t.name for t in a.tags.all()])
# 总计:1 (articles + JOIN author/category) + 1 (tags) = 2 次 SQL

步骤 7:查看实际执行的 SQL

# 方法 1:QuerySet.query
print(Article.objects.filter(status="published").query)

# 方法 2:DEBUG 模式下查看所有查询
from django.db import connection
articles = list(Article.objects.select_related("author").all()[:10])
for q in connection.queries:
    print(q["sql"], q["time"])

# 方法 3:django-debug-toolbar(推荐)
pip install django-debug-toolbar

讲清楚要点

  1. 双下划线是 Django 查询的灵魂:author__username 跨表 JOIN,views__gte 操作符。这是 Django ORM 与 SQL 之间的桥梁。
  2. QuerySet 惰性求值:链式调用不执行 SQL,只在迭代/切片/len/list 时执行。这让动态构建查询变得自然。
  3. filter vs exclude vs get:filter/exclude 返回 QuerySet(可继续链式),get 返回单个对象(找不到抛 DoesNotExist,找到多个抛 MultipleObjectsReturned)。
  4. aggregate vs annotate:aggregate 是「全局汇总」返回字典;annotate 是「按行附加统计字段」。两者常配合 values() 做 GROUP BY。
  5. select_related vs prefetch_related:FK/OneToOne 用 select_related(SQL JOIN,一次查完);M2M/反向 FK 用 prefetch_related(两次查询,Python 组装)。选错类型性能反而更差。
  6. values() 和 values_list():只取需要的字段,不构造完整 Model 实例,内存占用更小。values_list(flat=True) 直接返回一维列表。
  7. only() 和 defer():只加载部分字段(大 TextField 场景),但访问未加载字段会再次查询,慎用。

你可能会懵的地方

  • filter 之后还能 filter 吗? 能。Article.objects.filter(status="published").filter(views__gt=100) 等价于 filter(status="published", views__gt=100),AND 关系。要 OR 必须用 Q 对象。

  • 跨表查询性能会不会很差? author__username 会生成 JOIN,本身没问题。但如果不加索引,JOIN 大表会很慢。给 author_id、category_id 这些外键字段加索引(Django 默认会给 FK 加索引)。

  • get() 抛异常怎么办? 用 get_object_or_404(Article, id=pk)(视图里),或 try/except 捕获 Article.DoesNotExist。

  • first() 和 get() 区别? first() 找不到返回 None,get() 找不到抛异常。first() 更安全但会掩盖「应该有数据却没有」的 bug。

  • distinct() 用在什么时候? M2M 或一对多 JOIN 后行数会膨胀。Article.objects.filter(tags__name="python").distinct() 去重。代价是性能,能用子查询就不用 distinct。

错法对照

错法问题正解
循环里访问 a.author.nameN+1 查询select_related("author")
循环里 a.tags.all()M2M 的 N+1prefetch_related("tags")
article.views += 1; article.save()并发下数据覆盖update(views=F("views")+1)
Article.objects.all() 后转 list 再过滤内存中过滤,丢失数据库索引数据库层 filter
用 len(qs) 统计数量把所有行拉回来数qs.count() 用 SQL COUNT
if qs: 判断是否有数据把所有行拉回来判断qs.exists() 用 SQL EXISTS

卡点急救

  1. 查询慢得离谱:先看 connection.queries 数 SQL 数量,十有八九是 N+1。再看是不是没加索引,用 Meta.indexes 给常查询字段加上。
  2. Cannot resolve keyword 'xxx' into field:字段名拼错或跨表查询的关系字段没写对。检查 models.py 里的字段名和 related_name。
  3. ** annotate 后字段在模板里读不到**:annotate 加的是 Python 属性不是数据库列,模板里 {{ obj.article_count }} 直接用即可,不需要 save。

面试常问

  • 如何找出慢查询并优化? 开 DEBUG 看 connection.queries 或装 django-debug-toolbar 看每个请求的 SQL 数量和耗时。优化顺序:消除 N+1(select_related/prefetch_related)→ 加索引 → 用 values/only 减少字段 → 必要时 raw SQL。

  • aggregate 和 annotate 的区别?给一个用 annotate 的实际场景。 aggregate 返回汇总值(如全站文章总数),annotate 给每行附加统计字段。场景:作者列表页显示每个作者的文章数和最近发文时间:User.objects.annotate(article_count=Count("articles"), latest=Max("articles__created_at"))。

  • F 表达式和 Q 表达式分别解决什么问题? F 表达式:在 SQL 层引用字段值,避免「读出-修改-写回」的并发问题,常用于计数器。Q 表达式:构建 OR/NOT 等复杂查询条件,突破 filter 关键字参数只能 AND 的限制。

自测三问

  1. 一个文章列表页要显示「标题、作者名、分类名、标签列表、评论数」,最少需要几条 SQL?怎么写?
  2. filter(a=1).filter(b=2) 和 filter(Q(a=1) | Q(b=2)) 的区别?
  3. 为什么 update(views=F("views")+1) 比读出再 save 更安全?

小结一句话

Django QuerySet 用双下划线做跨表查询,用 Q/F 表达式组合复杂条件,用 select_related/prefetch_related 消除 N+1;掌握这三板斧,绝大多数业务查询都能高效完成。

延伸

  • Django 官方文档 - 执行查询
  • Django 官方文档 - 聚合
  • 下一课:模板 DTL——把查询结果渲染成 HTML。
加载本节练习…
← 上一课模型与 ORM下一课 →模板 DTL