上一篇看到访问一个被 .only() 排除的字段会偷偷多打一次查询——那是"惰性"在单个字段上的体现。这一篇把镜头拉远,看惰性求值在整个 QuerySet 级别怎么工作:链式调用几个过滤条件,到底是在干什么;真正触发查询的是哪一步;同一个查询结果又是怎么被缓存、不会重复查两次的。全部结论都用本机真实 SQL 日志核对过,不是看文档转述。
本机真实建表插入 5 条数据,监控 connection.queries 的真实变化。
qs = Article.objects.filter(views__gte=10)
print('after .filter(): queries =', len(connection.queries))
qs = qs.exclude(views__gte=40)
print('after .exclude(): queries =', len(connection.queries))
qs = qs.order_by('-views')
print('after .order_by(): queries =', len(connection.queries))
results = list(qs)
print('after list(qs): queries =', len(connection.queries))
print(connection.queries[-1][\"sql\"])
after .filter(): queries = 0
after .exclude(): queries = 0
after .order_by(): queries = 0
after list(qs): queries = 1
SELECT ... FROM article WHERE (views >= 10 AND NOT (views >= 40))
ORDER BY views DESC
三次链式调用,一次查询都没触发——它们做的事情是"往一个查询描述里追加条件",不是"执行查询"。真正的 SQL 直到 list(qs) 才拼出来,而且是一次性把三个条件合并成一条语句执行,不是查一次、过滤一次、排序一次这样分三步。
list()、for 循环、bool()、len()——这些"看起来在用普通序列"的操作,背后全部走同一个函数。
django/db/models/query.py · django @ 6.0.7, L371 def __len__(self): self._fetch_all() return len(self._result_cache) def __iter__(self): self._fetch_all() return iter(self._result_cache) def __bool__(self): self._fetch_all() return bool(self._result_cache)
django/db/models/query.py · django @ 6.0.7, L1998 def _fetch_all(self): if self._result_cache is None: self._result_cache = list(self._iterable_class(self)) ...
只有一个真正的检查点:_result_cache 是不是 None。第一次访问时是 None,于是真的执行查询、把结果存进去;之后不管再访问多少次,只要 _result_cache 还在,就直接用缓存,不会再碰数据库——这解释了为什么上面那次实验里,链式调用的三步全是"追加条件"("惰性"),而 list() 是"第一次有人真的要结果了"("求值")。
list(qs) # 第一次求值
for a in qs: # 再遍历一次同一个对象
pass
print('queries after second iteration:', len(connection.queries))
queries after second iteration: 1
还是 1 次——第二次遍历完全没有新查询,直接用的是 _result_cache 里已经存好的结果。QuerySet 表现得像一个"第一次用的时候才去算,算完就记住"的容器。
既然 qs = qs.exclude(...) 这种写法很像"原地修改",要确认一下:.filter() 到底是改了原对象,还是造了个新的?
base = Article.objects.all()
filtered = base.filter(views__gte=20)
print('base is filtered?', base is filtered)
list(filtered)
print('filtered._result_cache is populated:', filtered._result_cache is not None)
print('base._result_cache is still:', base._result_cache)
base is filtered? False
filtered._result_cache is populated: True
base._result_cache is still: None
base 和 filtered 是两个不同的对象,求值 filtered 完全不影响 base 的缓存状态——它俩各有各的 _result_cache。源码里 .filter() 的文档字符串写得很直接:
django/db/models/query.py · django @ 6.0.7, L1536 def filter(self, *args, **kwargs): """ Return a new QuerySet instance with the args ANDed to the existing set. """ ... return self._filter_or_exclude(False, args, kwargs)
django/db/models/query.py · django @ 6.0.7, L1979 def _clone(self): """Return a copy of the current QuerySet. A lightweight alternative to deepcopy().""" c = self.__class__(model=self.model, query=self.query.chain(), ...) ... return c
.filter()/.exclude()/.order_by() 内部都会先 _clone() 出一个新对象(带着查询描述的一份拷贝),再修改这个新对象,原对象从头到尾没被碰过。这也是为什么"惰性"是安全的:可以放心地把同一个 base 拿去派生出好几个不同方向的查询,互相之间不会打架。
"求值"不是只有一种形态——有些操作压根不需要把整个结果集搬出来。
print(Article.objects.filter(views__gte=20).exists()) print(connection.queries[-1]['sql']) print() print(Article.objects.filter(views__gte=20).count()) print(connection.queries[-1]['sql'])True SELECT 1 AS "a" FROM article WHERE views >= 20 LIMIT 1 3 SELECT COUNT(*) AS "__count" FROM article WHERE views >= 20
.exists() 是 SELECT 1 ... LIMIT 1,数据库找到第一条匹配就能停;.count() 是 SELECT COUNT(*),交给数据库自己数,压根不会把行数据传回 Python 这边。两个都不会碰 _result_cache,也都不会把完整的模型对象实例化出来——这也是为什么"判断存在用 .exists(),别写 if list(qs):"是真实有性能依据的建议。
最容易被忽略的一个细节:qs[3:6] 到底是不是查询数据库的 LIMIT/OFFSET,要看这个 qs 之前有没有被求值过。
qs = Article.objects.all().order_by('id')[3:6] # 还没求值就切片
print(list(qs))
print(connection.queries[-1]['sql'])
[...]
SELECT ... FROM article ORDER BY id ASC LIMIT 3 OFFSET 3
qs2 = Article.objects.all().order_by('id')
list(qs2) # 先强制求值,填满 _result_cache
before = len(connection.queries)
sliced = qs2[3:6] # 再切片
print('新增查询数:', len(connection.queries) - before)
print(type(sliced))
新增查询数: 0
<class 'list'>
在还没求值的 QuerySet 上切片,切片信息会被编译进真实 SQL 的 LIMIT/OFFSET 里,数据库只返回需要的那几行;在已经求值过(_result_cache 已经填满)的 QuerySet 上切片,直接是 Python 原生 list 在内存里切,一次新查询都不会有。同样是 qs[3:6] 这行代码,实际发生的事完全取决于它执行时 _result_cache 是不是 None。
复现前面"filter → exclude → order_by → 求值 → 再求值 → 求值另一个中间对象"的完整场景。
数据来自独立验证过的 Python 模拟——每个链式调用产生的都是独立对象(用真实的 _clone() 思路复现),哪次求值是真查、哪次是命中缓存,用一套独立回放算法核对过。