在实际应用中,Django 处理大量数据时常见的问题是内存激增、数据库负担加重或循环执行多次 SQL 查询。本文系统地介绍了如何在不改变业务逻辑的情况下,通过优化 QuerySet、精简字段、流式读取和分批写入等方法,将批量任务的内存使用和处理时间控制在一个可接受范围内。
一、为什么会内存激增
records = IcPmPrivateInfo.objects.filter(status=1)
表面上没有问题,但这里
records
在首次遍历时会一次性将所有结果加载到内存中。
Django 的 QuerySet 虽然是惰性对象(lazy),但在执行 SQL 之后,默认会缓存所有结果,以便你可以多次遍历:
list(records) # 全部加载 + 缓存
这在处理十万条数据时意味着:所有对象实例、字段和关联对象都会进入内存。如果每个对象占用几十 KB 的空间,内存可能会瞬间达到几百 MB。
二、使用 iterator() 控制内存增长
.iterator()
让 QuerySet 在迭代时按块从数据库读取数据,而不是一次性加载所有数据。
for record in IcPmPrivateInfo.objects.filter(status=1).iterator(chunk_size=2000):
process_record(record)
Django 会在后台创建数据库游标(PostgreSQL 使用 server-side cursor),每次取 2000 条再加载下一批。数据不缓存,内存占用接近恒定。
不能对
.iterator()
的结果再次遍历,因为它不会缓存数据。
在 MySQL 下,
.iterator()
仍然会分批取数,但底层不是 server-side cursor,而是普通游标 + 分批 fetch。
推荐实践:
- 大数据量批处理任务中一定要用
iterator()
chunk_size
设置在 1000–5000 比较合理
- 对内存敏感的任务,可进一步配合
values()
三、使用 values() / values_list() 减少对象开销
如果业务只需要部分字段,直接取字典或元组可以显著减少内存。
for row in IcPmPrivateInfo.objects.filter(status=1).values('id', 'amount').iterator(chunk_size=2000):
handle(row['id'], row['amount'])
或者:
for id, amount in IcPmPrivateInfo.objects.filter(status=1).values_list('id', 'amount'):
handle(id, amount)
这类查询:
- 不构造 Model 实例
- 只加载需要的字段
- 速度更快、内存更低
四、精简字段:only() 与 defer() 的机制
only() —— 只加载指定字段
qs = IcPmPrivateInfo.objects.only('id', 'name')
for obj in qs.iterator(chunk_size=2000):
print(obj.id, obj.name)
- SQL 只加载指定的字段
- 未加载的字段访问时会触发额外查询
- 减少内存消耗,但需要注意延迟字段访问可能引发大量额外查询
defer() —— 延迟指定字段加载
qs = IcPmPrivateInfo.objects.defer('large_json_field', 'remark')
for obj in qs.iterator(chunk_size=2000):
print(obj.id, obj.name)
- SQL 加载除延迟字段外的所有字段
- 延迟字段访问时才查询
- 对大字段或 JSON 类型的字段非常适用
区别表
| 方法 |
含义 |
默认行为 |
适用场景 |
| only() |
只取指定字段 |
其他字段延迟加载 |
知道确切需要的字段 |
| defer() |
延迟指定字段 |
其他字段立即加载 |
排除大字段或不常用的字段 |
五、适度使用 select_related / prefetch_related
select_related
:一对一或外键关系,JOIN 一次取完;
prefetch_related
:一对多或多对多,额外查询 + Python 拼接。在批量任务中不建议大量使用,以免 SQL JOIN 或 Python 内存膨胀。
六、批量写入与更新
- 批量写入
batch = []
for row in dataset:
batch.append(Result(...))
if len(batch) >= 1000:
Result.objects.bulk_create(batch)
batch = []
- 批量更新
Result.objects.bulk_update(objects, ['status', 'updated_at'], batch_size=500)
减少循环保存,性能显著提升。
七、大数据分页读取
last_id = 0
page_size = 2000
while True:
rows = list(
IcPmPrivateInfo.objects
.filter(status=1, id__gt=last_id)
.order_by('id')[:page_size]
)
if not rows:
break
for r in rows:
process(r)
last_id = rows[-1].id
- 防止长时间事务或长游标带来的压力;
- 内存占用保持稳定;
- 支持断点续处理。
八、性能对比表(优化前后)
| 处理方式 |
内存占用 |
CPU 消耗 |
SQL 查询次数 |
备注 |
| 未优化 QuerySet |
约2–3 GB |
高 |
1 次大查询 + 迭代缓存 |
内存急剧增长,可能 OOM |
iterator() + values() |
约100 MB |
中等 |
分批 50 次左右 |
内存稳定,CPU 平均 |
only()/defer() + iterator() |
约80 MB |
中等 |
分批 50 次 + 延迟查询少量额外 |
对大字段优化显著 |
select_related/prefetch_related |
约150–300 MB |
中高 |
1~2 次 JOIN/额外查询 |
适合关联少量对象,否则内存增加 |
bulk_create/bulk_update |
— |
— |
批量写入 SQL 数量少 |
写入性能提升 10~20 倍 |
九、总结
内存优化:
iterator()
+
values()
+
only()
/
defer()
- 性能优化:批量写入、批量更新、分页读取;
- 数据库压力:分批查询、索引优化、避免循环查询;
- 关联对象处理:
select_related
/
prefetch_related
谨慎使用,防止 JOIN 增长。
通过上述方法,即使处理百万级数据,也能确保内存稳定、SQL 查询可控,保障批量任务在生产环境中高效运行。