QuerySet 是惰性查询计划,调用 filter 等方法仅构建未执行的 SQL 描述,链式调用不触发查询,直到 list()、for 循环等求值操作才执行;qs[0] 求值而 qs[5:10] 仍惰性,str(qs.query) 查计划,connection.queries 确认是否真查库。

QuerySet 不是数据,是“待执行的查询计划”
你调用 Book.objects.filter(title__contains="Django") 的那一刻,数据库里什么都没发生。它返回的不是列表,也不是对象集合,而是一个尚未执行的查询描述——就像写好但没按回车的 SQL 语句。
这个设计直接决定了你怎么用它:可以反复叠加条件、改排序、加预加载,只要不真正要数据,就零开销。
- 常见错误现象:在循环里反复写
qs = MyModel.objects.filter(...),以为每次都在“重查”,其实只是新建轻量 QuerySet 对象;真慢的其实是后面某次list(qs)或 for 循环迭代 - 使用场景:构建动态搜索接口时,根据请求参数逐步拼接过滤条件,比如
qs = qs.filter(status=active)、qs = qs.exclude(deleted=True),全程不碰数据库 - 性能影响:链式调用 10 次
filter()和只调 1 次,最终生成的 SQL 是一样的(单条 SELECT),没有 N+1 查询风险——前提是别提前求值
哪些操作会偷偷触发数据库查询(即“求值”)
惰性不是永久隐身,一旦你对 QuerySet 做了某些事,它立刻执行 SQL 并缓存结果。这些动作就是“求值点”,也是最容易踩坑的地方。
- 典型求值操作:
list()、len()、bool()(比如if qs:)、repr()(打印调试时)、for 循环迭代、first()、last()、get() - 容易忽略的陷阱:
qs[0]会求值(取第一个元素),但qs[5:10]不会(切片本身仍是惰性的,除非你遍历它) - 兼容性注意:Django 4.2+ 中
qs.exists()虽然也求值,但生成的是EXISTS子句,比len(qs) > 0高效得多;后者会先执行完整 SELECT 再算长度
链式调用不是“函数组合”,而是 QuerySet 克隆与 query 修改
每次调用 filter()、order_by()、select_related(),Django 都会克隆当前 QuerySet,复制其内部的 query 对象,并在副本上追加条件——原 QuerySet 不变,新旧互不影响。
- 实操建议:如果你需要复用基础查询,比如“所有已发布文章”,可定义为
published_qs = Article.objects.filter(published=True),再分别派生published_qs.filter(category="tech")和published_qs.order_by("-created_at"),安全且清晰 - 参数差异:
exclude()不是filter().exclude()的简单反向,它会影响 WHERE 子句的逻辑结构,尤其涉及 OR/AND 混合时,结果可能和直觉不符 - 性能影响:过度嵌套链式调用本身不慢,但若中间夹杂了
annotate()或prefetch_related(),要注意字段别名冲突或 JOIN 爆炸——比如连续两次annotate(total=Sum(...))可能导致 SQL 报错
调试时怎么确认 QuerySet 真的没查库?
光看代码没法 100% 确认,得靠工具验证。最直接的办法是打开 Django 的 SQL 日志,或用 str(qs.query) 看生成的 SQL——但它只显示“计划”,不代表已执行。
- 可靠方法:在视图开头加
from django.db import connection; print(len(connection.queries)),执行 QuerySet 后再打一次,差值为 1 才说明真查了 - 常见误判:用
print(qs)会触发repr()→ 求值;想看结构请用print(qs.query)(注意这是未格式化的 raw SQL 字符串) - 容易被忽略的地方:第三方库如 Django Debug Toolbar 默认只显示“已执行”的查询,不会告诉你某个 QuerySet 被构造了但始终没用——这种“幽灵 QuerySet”既不耗资源也不报错,却可能暴露业务逻辑冗余










