bigywater.tech
Django · 27

QuerySet 的惰性执行与结果缓存

QuerySet 到底什么时候查库?第二次迭代还查吗?count() 和 len() 有什么区别?这篇用 CaptureQueriesContext 把每个操作发了几次查询、发的是什么 SQL 都数出来。

0 次
构造与链式调用 filter / order_by / exclude 不发任何查询
LIMIT 21
repr(qs)(比如在 shell 里回车)会发一次 LIMIT 21 的查询
缓存
迭代后结果缓存在 QuerySet 里;exists() 与 iterator() 不填充它

什么时候真的查库

实测 · 本机执行输出
构造 + 链式调用 filter/order_by/exclude                    查询 0 次
第 1 次 list(qs)                                       查询 1 次
第 2 次 list(qs)(结果已缓存)                                查询 0 次
len(qs)(已缓存)                                         查询 0 次
qs[0](已缓存)                                           查询 0 次
qs.count()(已缓存)                                      查询 0 次
qs.exists()(已缓存)                                     查询 0 次

惰性:只有「求值」才查询(迭代、切片带步长、list()、len()、bool() 等)。求值一次后结果缓存在 _result_cache,同一个对象的后续访问不再查库。

没缓存时每个操作的 SQL

实测 · 本机执行输出
Item.objects.all()[0]              1 次: SELECT "lab_item"."id", "lab_item"."name", "lab_item"."price" FROM "lab_item" LIMIT 1
Item.objects.all()[5:10] 并迭代       1 次: SELECT "lab_item"."id", "lab_item"."name", "lab_item"."price" FROM "lab_item" LIMIT 5 OFFSET 5
qs.count()                         1 次: SELECT COUNT(*) AS "__count" FROM "lab_item" WHERE "lab_item"."price" > 10
qs.exists()                        1 次: SELECT 1 AS "a" FROM "lab_item" WHERE "lab_item"."price" > 10 LIMIT 1
bool(qs)                           1 次: SELECT "lab_item"."id", "lab_item"."name", "lab_item"."price" FROM "lab_item" WHERE "lab_item"."price" > 10
len(qs)                            1 次: SELECT "lab_item"."id", "lab_item"."name", "lab_item"."price" FROM "lab_item" WHERE "lab_item"."price" > 10
repr(qs)(比如在 shell 里回车)            1 次: SELECT "lab_item"."id", "lab_item"."name", "lab_item"."price" FROM "lab_item" LIMIT 21

判断「有没有」用 exists()(LIMIT 1),不要 bool(qs) 或 len(qs)(会取回全部行)。只要个数用 count()。

哪些操作会填充缓存

实测 · 本机执行输出
先 qs.exists()                                        查询 1 次
再 list(qs)                                           查询 1 次
再 qs.exists()(有缓存了)                                  查询 0 次
新 qs:先 if qs2: (bool)                                查询 1 次
再 qs2.count()(缓存)                                    查询 0 次
新 qs:qs3.iterator() 遍历一遍                             查询 1 次
再 list(qs3)(iterator 不填充缓存)                          查询 1 次

一个常见的写法 if qs.exists(): for x in qs: 发两次查询;如果本来就要迭代,直接 list(qs) 再判断更省。

缓存的副作用

实测 · 本机执行输出
第一次 list(qs): []
新建一条之后再 list(qs)(同一个对象): []
qs.all() 生成新的 QuerySet 再取: [<Item: Item object (51)>]
qs.filter() 之后: [<Item: Item object (51)>]

长期持有的 QuerySet(模块级变量)会一直返回第一次的结果,这是个常见 bug。要「重新查」就要生成新的 QuerySet。

分步演示:何时查库

QuerySet 惰性与缓存
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;计数用 CaptureQueriesContext
    • 没有验证:切片带步长(qs[::2])的求值、prefetch 结果的缓存、异步 QuerySet(aiterator)、__len__ / __bool__ 在其它数据库上的 SQL 差异
    京ICP备2026067502号-1京公网安备11010502063487号