bigywater.tech
Django · 102

iterator() 降低查询集内存占用

QuerySet 遍历时会把所有结果对象缓存在 _result_cache 里。表大了,一次 for 循环就是几百 MiB。iterator() 让 Django 分批取、用完即弃。这篇量一下 20 万行数据的内存和耗时,再看它与 prefetch_related 的配合。

155.0 → 1.7 MiB
20 万行遍历的峰值内存:list() → iterator()(SQLite)
153.4 → 1.7 MiB
同样的对比,在自己起的 PostgreSQL 上
chunk_size
iterator() 与 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 调小还能再降内存,代价是与数据库的往返更多(本文没有量化这一点)。

和 prefetch_related 一起用

实测 · 本机执行输出
  [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 用了服务端游标」这一机制本文没有验证——只能确认内存确实下降了。

分步演示:内存与耗时

iterator() 的效果
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。表 lab_row 20 万行,每行约 260 字节(整型、40 字符串、200 字符文本、外键);SQLite 与自己起的临时 PostgreSQL 16 各测一遍;内存用 tracemalloc,每种方式独立进程测
    • 没有验证:服务端游标的实际使用、MySQL 的行为、iterator() 的耗时随 chunk_size 变化、多线程并发遍历、DISABLE_SERVER_SIDE_CURSORS 设置(与 pgbouncer 事务池化的兼容)
    京ICP备2026067502号-1京公网安备11010502063487号