大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
查询与优化
这一课你将能做到
- 用 filter/exclude/annotate 写出等价于复杂 SQL 的 QuerySet 查询
- 用 aggregate + F/Q 表达式完成统计分析和条件组合查询
- 用
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
讲清楚要点
- 双下划线是 Django 查询的灵魂:
author__username跨表 JOIN,views__gte操作符。这是 Django ORM 与 SQL 之间的桥梁。 - QuerySet 惰性求值:链式调用不执行 SQL,只在迭代/切片/len/list 时执行。这让动态构建查询变得自然。
- filter vs exclude vs get:filter/exclude 返回 QuerySet(可继续链式),get 返回单个对象(找不到抛
DoesNotExist,找到多个抛MultipleObjectsReturned)。 - aggregate vs annotate:aggregate 是「全局汇总」返回字典;annotate 是「按行附加统计字段」。两者常配合
values()做 GROUP BY。 - select_related vs prefetch_related:FK/OneToOne 用 select_related(SQL JOIN,一次查完);M2M/反向 FK 用 prefetch_related(两次查询,Python 组装)。选错类型性能反而更差。
values()和values_list():只取需要的字段,不构造完整 Model 实例,内存占用更小。values_list(flat=True)直接返回一维列表。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.name | N+1 查询 | select_related("author") |
循环里 a.tags.all() | M2M 的 N+1 | prefetch_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 |
卡点急救
- 查询慢得离谱:先看
connection.queries数 SQL 数量,十有八九是 N+1。再看是不是没加索引,用Meta.indexes给常查询字段加上。 Cannot resolve keyword 'xxx' into field:字段名拼错或跨表查询的关系字段没写对。检查 models.py 里的字段名和related_name。- ** 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 的限制。
自测三问
- 一个文章列表页要显示「标题、作者名、分类名、标签列表、评论数」,最少需要几条 SQL?怎么写?
filter(a=1).filter(b=2)和filter(Q(a=1) | Q(b=2))的区别?- 为什么
update(views=F("views")+1)比读出再 save 更安全?
小结一句话
Django QuerySet 用双下划线做跨表查询,用 Q/F 表达式组合复杂条件,用 select_related/prefetch_related 消除 N+1;掌握这三板斧,绝大多数业务查询都能高效完成。
延伸
- Django 官方文档 - 执行查询
- Django 官方文档 - 聚合
- 下一课:模板 DTL——把查询结果渲染成 HTML。