为什么pandas.to_parquet保存DataFrame时内存占用远超数据实际大小?
问题根因
sys.getsizeof的测算结果完全不具备参考性:对于存储为objectdtype的pandas字符串列,sys.getsizeof只会统计DataFrame结构本身、以及列里存的Python对象指针的内存,不会递归统计指针指向的、存在堆上的实际字符串内容的内存。7300万行的英文句子列,按平均每句80-120字符算,仅原始字符串的实际内存占用就有60-90GB,远高于你测出来的25GB。- 两款parquet引擎写入时都会产生多份全量内存副本:不管是pyarrow还是fastparquet,写入前都需要把pandas的
object类型字符串转换成引擎自有的原生字符串存储结构,这个转换过程会生成一份和全量字符串等大的内存副本;默认写入逻辑下,引擎会先把全量数据转换完成后再统一做编码、写入,光原始数据+转换副本的内存占用就会达到实际数据大小的2倍以上。 - fastparquet直接抛溢出错误是历史遗留问题:fastparquet的字符串转换逻辑长期使用32位有符号整数做缓冲区长度计数,当单批处理的字符串总长度超过2^31-1字节(约2.1GB)时就会触发整数溢出,直接报错,根本走不到后续压缩、写入流程,这也是你改压缩参数完全不影响报错表现的原因——OOM和溢出都发生在压缩步骤之前,压缩参数还没生效就已经失败了。
- 默认行组配置进一步放大内存开销:pyarrow默认的行组大小通常在百万行级以上,每个行组做字典编码、RLE编码、压缩时还需要申请额外的编码缓冲区内存,叠加上前面的副本开销,总内存占用突破512GB是完全符合逻辑的。
验证与修复方案
- 先拿到真实内存数据:用
df.memory_usage(deep=True).sum()测算DataFrame实际内存占用,加了deep=True参数才会递归统计字符串等引用类型对象的真实内存,测完你就能看到实际大小远高于25GB。 - 替换字符串存储类型:提前把两列都转成pandas原生的
string类型,执行df = df.astype({'文本列名':'string', 'ID列名':'string'}),这种格式下字符串是连续存储在原生数组里的,不是分散的Python对象,转parquet引擎格式时不需要遍历拷贝大量零散Python对象,转换阶段的内存开销可以降低60%以上。 - 调小行组大小:调用
to_parquet时传入row_group_size=100000(即10万行一个行组),降低单批次编码、压缩的缓冲区内存占用。 - 超大数据集改用流式分批写入:如果调整上面两个参数后还是有内存压力,不要直接用DataFrame的
to_parquet接口,改用对应引擎的流式写入器(比如pyarrow的ParquetWriter),把DataFrame按100-500万行的粒度切分,逐批写入、逐批释放内存,全程不会生成全量数据副本,内存占用可以控制在单批大小的1.5倍以内。
内容的提问来源于stack exchange,提问作者Tim
相关产品推荐
相关产品推荐

