bigywater.tech
Django · 29

only()、defer() 触发的额外查询

大字段不想每次都查,用 only() 或 defer() 就完事了吗?一旦你后面又读到了它,会发生什么?这篇数出额外的查询次数,并量不同取法的耗时。

1 → 201
200 行 only("title") 只读 title 与又读了 body 的查询次数
12 → 1.2 ms
2000 行 × 10 KB:取全部字段 → values_list 单列的耗时
save()
带推迟字段的实例 save() 只更新已加载的列

生成的 SQL 与被推迟的字段

实测 · 本机执行输出
all()                          SELECT "id", "title", "body", "author_id" 
only('title')                  SELECT "id", "title" 
defer('body')                  SELECT "id", "title", "author_id" 
only('title', 'author__name')+select_related SELECT "id", "title", "author_id", "lab_author"."id", "lab_author"."name" FROM "lab_article" INNER JOIN "lab_author" ON ("author_id" = "lab_author"."id") LIMIT 1
only('title') 的实例 get_deferred_fields() = ['author_id', 'body']
defer('body') 的实例 get_deferred_fields() = ['body']

only() 是白名单(主键总是会取),defer() 是黑名单。only('title') 连外键 author_id 也推迟了,访问 a.author_id 或 a.author 都会额外查询。

读到推迟的字段

实测 · 本机执行输出
200 行 all() 读 body                                 查询     1 次        0.9 ms
200 行 only('title') 只读 title                       查询     1 次        0.5 ms
200 行 only('title') 又读了 body                       查询   201 次       22.9 ms

模板里循环访问一个被 defer 的字段,是这个坑的高发场景。发现时用 only() 列全要用的字段,或直接 values() / values_list()。

耗时

实测 · 本机执行输出
Article.objects.all()(取全部字段)                       查询     1 次       12.5 ms
defer('body')                                      查询     1 次        8.9 ms
only('title')                                      查询     1 次        5.1 ms
values_list('title', flat=True)                    查询     1 次        1.2 ms

SQLite 在本机,没有网络往返,所以字段体积带来的差别主要是读取和构造对象的代价;换成远程数据库,大字段的传输代价会更明显(本文没有测)。

save() 与 refresh_from_db()

实测 · 本机执行输出
only("title") 实例 save() 的 SQL: UPDATE "lab_article" SET "title" = 'changed' WHERE "lab_article"."id" = 1
refresh_from_db(fields=["body"]) 的 SQL: SELECT "lab_article"."id", "lab_article"."body" FROM "lab_article" WHERE "lab_article"."id" = 1 LIMIT 21  | 之后 deferred = ['author_id']

对推迟了字段的实例调用 save(),Django 只更新已加载的字段,不会误把推迟的字段写掉。

分步演示:only / defer

only() 与 defer()
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;耗时取 5 轮最快
    • 没有验证:PostgreSQL / MySQL 下大字段的实际传输代价、only() 与多态继承 / 代理模型的交互、defer 用在 select_related 的关联字段上、被推迟字段在 pickle / 序列化中的行为
    京ICP备2026067502号-1京公网安备11010502063487号