QuerySet 是惰性的,而模板是最容易「不知不觉」触发查询的地方。这篇用 12 个小模板,把每种写法发了几次查询数清楚,尤其是 __str__ 和反向关系里的 N+1。
for p in posts(遍历一次) 1 次
遍历两次(同一个 QuerySet 对象) 1 次
{% if posts %} 再 for 1 次
{{ posts.count }} + for + {{ posts.count }} 2 次
{{ posts|length }} + for 1 次
{% for p in posts.all %} 写两次(每次 .all 产生新 QuerySet) 2 次
{{ posts.0.title }} 和 {{ posts.first.title }} 2 次
{% for %}{% empty %} 1 次模板语言里 posts.count 是方法调用(没有括号),posts.0 是下标。它们对「还没求值」的 QuerySet 各发一次查询。
p.author.name(不优化) 21 次
p.author.name + select_related("author") 1 次
{{ p }}(__str__ 里用了 self.author.name)不优化 21 次
{{ p }} + select_related("author") 1 次
a.posts.count(每个作者一次 COUNT) 6 次
a.posts.all|length(每个作者一次查询) 6 次
a.posts.all|length + prefetch_related("posts") 2 次
a.posts.count + prefetch_related("posts")(count 用预取好的缓存) 2 次值得注意的是 __str__:admin 的列表页、下拉框、日志里打印对象,都会调用它。如果 __str__ 里访问了外键,每一行都会多一次查询。另外一个细节:在预取过的关系上 .count 用的是预取缓存(实测 2 次),而没有预取时每个作者一次 COUNT。
传 list 给 iter_twice.html: 0 次(list 已经在内存里) 传 list 给 length.html: 0 次
这不是通用建议,但它让「模板会查多少次库」变得可预测。