bigywater.tech
Django · 91

模板中的 QuerySet 惰性执行问题

QuerySet 是惰性的,而模板是最容易「不知不觉」触发查询的地方。这篇用 12 个小模板,把每种写法发了几次查询数清楚,尤其是 __str__ 和反向关系里的 N+1。

1 次
同一个 QuerySet 在模板里遍历几次都只查 1 次(结果缓存)
21 → 1
遍历里访问 p.author.name(或 __str__ 里用外键):不优化 → select_related
.all / .first / [0]
每次都产生新的查询,不共享缓存

QuerySet 在模板里的求值

实测 · 本机执行输出
  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 各发一次查询。

藏在关系与 __str__ 里的 N+1

实测 · 本机执行输出
  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 次

这不是通用建议,但它让「模板会查多少次库」变得可预测。

分步演示:模板里的查询

模板里的 QuerySet
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;用 render_to_string + CaptureQueriesContext 数查询
    • 没有验证:{% include %} 里的重复求值、自定义 tag 里的查询、模板缓存({% cache %})对查询的影响(第 95 篇)、Jinja2 后端的行为
    京ICP备2026067502号-1京公网安备11010502063487号