QuerySet 遍历时会把所有结果对象缓存在 _result_cache 里。表大了,一次 for 循环就是几百 MiB。iterator() 让 Django 分批取、用完即弃。这篇量一下 20 万行数据的内存和耗时,再看它与 prefetch_related 的配合。
[default] list(qs) 727 ms 峰值内存 155.0 MiB → 200000 [default] for r in qs(结果缓存在 QuerySet 里) 726 ms 峰值内存 155.0 MiB → 200000 [default] for r in qs.iterator() 564 ms 峰值内存 1.7 MiB → 200000 [default] for r in qs.iterator(chunk_size=500) 562 ms 峰值内存 0.5 MiB → 200000 [pg] list(qs) 782 ms 峰值内存 153.4 MiB → 200000 [pg] for r in qs(结果缓存在 QuerySet 里) 684 ms 峰值内存 153.4 MiB → 200000 [pg] for r in qs.iterator() 607 ms 峰值内存 1.7 MiB → 200000 [pg] for r in qs.iterator(chunk_size=500) 617 ms 峰值内存 0.4 MiB → 200000
峰值内存用 tracemalloc 测得(单独一次运行,不与计时混在一起)。list(qs) 与普通 for 都把 20 万个模型对象留在内存里;iterator() 只保留当前批次,内存基本与总行数无关。chunk_size 调小还能再降内存,代价是与数据库的往返更多(本文没有量化这一点)。
[default] 不给 chunk_size → ValueError: chunk_size must be provided when using QuerySet.iterator() after prefetch_related(). [default] chunk_size=20000,处理 6 万行(3 批)的查询数: 4 其中预取 tag 的查询: 3 [pg] 不给 chunk_size → ValueError: chunk_size must be provided when using QuerySet.iterator() after prefetch_related(). [pg] chunk_size=20000,处理 6 万行(3 批)的查询数: 4 其中预取 tag 的查询: 3
Django 4.1 起 iterator() 支持 prefetch_related,前提是显式给出 chunk_size;预取是每一批做一次。
两次 iterator()(各只取第一条):查询 2 次(每次都新发查询) 普通遍历两次:查询 1 次(第二次用缓存)
拿到 iterator() 就只能消费一遍。如果之后还要再遍历同一批数据,要么再查一次,要么就不要用 iterator()。
我在 PostgreSQL 上另外查了一次:iterator() 取完第一批后,从另一个连接看 pg_stat_activity,没有处于非 idle 的语句(输出 0)。但我没有查 pg_cursors,所以「Django 用了服务端游标」这一机制本文没有验证——只能确认内存确实下降了。