全部版块 我的主页
论坛 数据科学与人工智能 IT基础
45 0
2025-11-14

在实际应用中,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 内存膨胀。

六、批量写入与更新

  1. 批量写入
batch = []
for row in dataset:
    batch.append(Result(...))
if len(batch) >= 1000:
    Result.objects.bulk_create(batch)
    batch = []
  1. 批量更新
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 查询可控,保障批量任务在生产环境中高效运行。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群