3.1 QuerySet 查询全景 本节摘要:QuerySet 是 Django ORM 的核心抽象:它先是一份"查询说明书",被真正需要时才翻译成 SQL 执行。本节讲透惰性求值与缓存这两个心智模型,过一遍过滤、跨关系、聚合三大类查询 API,最后覆盖写操作与事务边界。看懂 ORM 语句如何变成 SQL,后面所有的性能问题都能自己诊断。 惰性求值:最重要的一个概念 先看一段"什么都没发生"的代码: 前三行只是在组装查询说明书。过滤器、排序、切片都可以继续往上叠,因为它们都返回新的查询集。直到迭代发生,ORM 才把说明书翻译成一条 SQL 交给数据库。这些动作会触发求值:迭代、list 包装、len 计数、bool 判断、切片后再次切片之外的正向索引、序列化输出。
本节摘要:QuerySet 是 Django ORM 的核心抽象:它先是一份"查询说明书",被真正需要时才翻译成 SQL 执行。本节讲透惰性求值与缓存这两个心智模型,过一遍过滤、跨关系、聚合三大类查询 API,最后覆盖写操作与事务边界。看懂 ORM 语句如何变成 SQL,后面所有的性能问题都能自己诊断。
先看一段"什么都没发生"的代码:
qs = Article.objects.filter(status=Article.PUBLISHED) qs = qs.filter(created_at__year=2026).order_by("-created_at")[:10] # 到这里为止:数据库没收到任何 SQL for article in qs: # 此刻才生成并执行 SQL print(article.title)
前三行只是在组装查询说明书。过滤器、排序、切片都可以继续往上叠,因为它们都返回新的查询集。直到迭代发生,ORM 才把说明书翻译成一条 SQL 交给数据库。这些动作会触发求值:迭代、list 包装、len 计数、bool 判断、切片后再次切片之外的正向索引、序列化输出。
理解这一点,很多"诡异现象"立刻正常。比如这段代码执行了两条 SQL:
articles = Article.objects.all() if articles: # 求值一次:SELECT ... LIMIT 1 titles = [a.title for a in articles] # 求值第二次:完整 SELECT
补救办法是先 list 落地,后续判断与遍历都走内存副本。每个执行过的查询集会把结果缓存在自己身上,同一个查询集的第二次迭代不会重新查库——但 filter 一接龙,新查询集的缓存就另起炉灶。

过滤类。 filter 与 exclude 互为正反,条件用"字段名加双下划线加查找器"表达:
Article.objects.filter(title__icontains="django") # 标题含 django,不区分大小写 Article.objects.filter(created_at__gte=some_date) # 大于等于 Article.objects.filter(title__startswith="深") # 前缀 Article.objects.filter(id__in=[1, 9, 20]) # 集合成员 Article.objects.filter(body__isnull=False) # 非空
多个条件传进同一个 filter 是"与",链式 filter 也是"与",要用"或"就上 Q 对象:
from django.db.models import Q Article.objects.filter( Q(title__icontains="orm") | Q(body__icontains="orm") )
跨关系类。 双下划线还能穿过外键,直接用"对方模型"的字段过滤或取值:
Article.objects.filter(category__name="Python") # 按分类名过滤文章 Article.objects.values("author__username") # 直接取作者用户名 Category.objects.filter(articles__isnull=True) # 反向:没有文章的分类
聚合类。 聚合函数把多行折叠成一个值,annotate 给每行附加一个计算列:
from django.db.models import Count, Avg Comment.objects.count() # 总评论数 Article.objects.aggregate(total=Count("comments")) # {'total': 512} Category.objects.annotate( # 每个分类的文章数,常用于侧栏统计 article_count=Count("articles", distinct=True) ).filter(article_count__gte=10)
annotate 后接 filter 是"分组后再过滤",对应 SQL 的 HAVING,侧栏"热门分类"就是这条查询。
💡 调试期在配置里把日志级别调到 DEBUG,控制台会打印每条真实 SQL 与耗时。开发时开着它,ORM 就是一面透明玻璃。
读之外的四件大事:
art = Article.objects.create(title="ORM 全景", body="...", author=u) # 建并存 Article.objects.filter(status=Article.DRAFT).update(status=0) # 批量更新 n, _ = Comment.objects.filter(article_id=99).delete() # 批量删除 art.title = "改题"; art.save() # 实例保存
update 与 save 的选择有讲究:update 一条 SQL 改一批,但不触发模型的 save 方法与信号;逐个 save 触发钩子但 SQL 数量翻倍。批量改状态这类"纯数据"操作用 update,需要副作用(记日志、清缓存)的场景用 save。
多步写操作要包进事务,要么全成要么全不成。发文扣积分就是典型:
from django.db import transaction with transaction.atomic(): article = Article.objects.create(...) author.profile.article_count += 1 author.profile.save()
用了一段时间 ORM 后,值得刻意培养一种翻译感:每写一条查询集链,脑子里能大致浮现它将变成什么样的 SQL。过滤一个字段是条件子句,跨双下划线的过滤是连接查询,排序方法是排序子句,切片是数量限制。这种对应感平时不显山露水,两个时刻极其值钱:其一是排查慢查询时,看到 ORM 写法就能推断数据库将执行什么计划,索引该往哪里加;其二是评审他人代码时,一眼看出某个循环里的查询实际上是把连接查询拆成了几十条语句。培养方法是在开发期偶尔打开连接的调试输出,把常用查询模式的真实 SQL 看上几遍。ORM 的价值从来不是让你不看 SQL,而是让你大多数时候不必手写 SQL——前提是你始终知道它在替你写什么。
写操作的章节还差最后一块拼图:事务。单个保存天然原子,需要手工包边界的场景是"一组写操作必须同生共死"——发布文章并同时写入标签关联表,两步之间失败就会出现带标签残骸的半成品状态。框架提供的装饰器和上下文管理器都能划定边界,但更值得记的是尺度感:事务不是越大越安全,长事务会长时间持有行锁,高并发下反而制造阻塞。经验法则是让事务只包住"必须原子"的最小集合,查询准备放在边界之外,耗时操作(发通知、调外部接口)更是坚决移出——它们属于队列的领地,第 11 章会正式接管这类活。
会查了,下一章第一节就让视图调用这些查询——但先把 N+1 这个坑填了,免得带病上线。