Django3.2如何高效序列化大型QuerySet并写入JSON文件
Django 3.2 大查询集序列化导出JSON优化方案
运行环境:Django 3.2.10,Python 3.9
首先明确两种原始写法的性能问题:
- 全量传入序列化器的写法:会一次性加载10万条用户记录到内存,同时序列化器会生成完整的10万元素列表,内存占用会随数据量线性上涨,数据量更大时会直接触发内存溢出
- 单条循环实例化序列化器的写法:一是直接迭代QuerySet会触发默认的全量结果缓存,同样会把所有记录加载到内存;二是单条初始化序列化器、重复执行dump的序列化和IO开销极高,因此执行速度极慢
核心优化原则
- 用
iterator()分批拉取数据库记录,跳过QuerySet默认缓存,迭代完一批就释放一批内存,避免全量加载 - 只查询序列化需要的字段,杜绝无意义的字段拉取
- 批量处理序列化逻辑,减少重复的序列化器初始化和IO操作
- 无复杂逻辑时优先跳过模型实例化、DRF序列化器的额外开销,直接拿字典格式数据写入
方案1:需要保留DRF序列化器逻辑的场景
如果序列化过程涉及自定义字段、嵌套关联等必须用DRF序列化器的逻辑,按如下写法实现,10万条数据内存占用可稳定在50MB以内:
import json from django.core.serializers.json import DjangoJSONEncoder # 每批拉取的记录数,可根据服务器内存调整,建议1000-5000区间 CHUNK_SIZE = 2000 # 用only()指定需要序列化的字段,大幅降低数据库传输和内存占用;有关联字段提前加select_related/prefetch_related避免N+1 queryset = Users.objects.all().only("id", "username", "email", "join_time") with open("output.json", "w", encoding="utf-8") as f: f.write("[") is_first_item = True # 分批迭代,不触发QuerySet全量缓存 for batch in queryset.iterator(chunk_size=CHUNK_SIZE): # 对当前批次批量做序列化,比单条实例化序列化器效率高3倍以上 serialized_batch = MySerializer(batch, many=True).data for item in serialized_batch: if not is_first_item: f.write(",") json.dump(item, f, cls=DjangoJSONEncoder, ensure_ascii=False) is_first_item = False f.write("]")
方案2:纯字段导出的高性能场景
如果只是将模型字段直接导出为JSON,没有自定义序列化逻辑,直接用values()返回字典结构,跳过模型实例化和DRF序列化器的额外开销,10万条数据导出耗时可压缩到2秒内:
import json from django.core.serializers.json import DjangoJSONEncoder CHUNK_SIZE = 5000 # 直接指定需要导出的字段,返回字典格式,无需生成模型实例 queryset = Users.objects.values("id", "username", "email", "join_time") with open("output.json", "w", encoding="utf-8") as f: f.write("[") is_first_item = True for item in queryset.iterator(chunk_size=CHUNK_SIZE): if not is_first_item: f.write(",") json.dump(item, f, cls=DjangoJSONEncoder, ensure_ascii=False) is_first_item = False f.write("]")
避坑提示
- 使用
iterator()时不要将迭代结果追加到全局列表,否则会完全失去分批优化的意义,内存依然会持续上涨 - 禁止在循环迭代过程中查询关联表数据,提前用
select_related(一对一、外键)、prefetch_related(多对多、反向外键)预加载关联数据,避免N+1查询问题 - 写入文件时保持文件句柄常开,不要每条记录都执行打开/关闭文件操作,减少不必要的IO开销
- 序列化时间、UUID等Django特殊字段类型时,必须用
DjangoJSONEncoder做编码,否则会触发类型报错 - 不要为了图方便用
list(queryset)将整个查询集转为列表,会直接触发全量加载。
内容的提问来源于stack exchange,提问作者Vadim Beglov
相关产品推荐
相关产品推荐

