拓冰建站拓冰建站
首页 / 资讯中心 / 正文

30GB CSV 转 3GB Parquet:DuckDB 高效压缩与转换指南

1. 30GB的CSV浪费到底在哪手里有个30GB的CSV这玩意儿在我的工作里太常见了。往往是某个业务方的数据导出、某个爬虫抓下来的原始日志、或者一个研究机构放的公开数据集长得都差不多第一行是列名往下几百上千万行全是逗号分隔的值看一眼就头大。先说结论CSV这种格式天生就不适合做大文件存储。30GB看着唬人实际上里面一大半都是原样存下来的冗余信息你随便换个存储格式压到5GB以内很正常压得好甚至能到3GB。这不是什么黑科技纯粹是CSV在设计上就没考虑过“效率”这两个字。1.1 文本存储的天然浪费CSV的本质是纯文本文件一切数据都以字符形式保存。数字123456789.123456在CSV里就是明晃晃的16个字节一个不多一个不少。但你仔细想想一个双精度浮点数在计算机里的二进制表示其实只需要8个字节。光是数字这一项CSV就多浪费了整整一倍的空间。文本里到处都是不可见的浪费。一个空字段CSV里也得用两个逗号占位比如1,,3中间那个空值就用掉了2个字节。字符串更夸张如果你导出的数据里带几个制表符、换行符因为CSV规范要求这些字符必须用引号包起来原本1个字节的换行符在CSV里就变成了\n这种2个字符的转义再套上引号直接翻倍。处理过脏数据的人都知道一个字段里如果混进了逗号、引号、换行导出CSV时那个转义逻辑有多恶心而这些额外的转义字符在存储上全是白花花的空间浪费。我实测过一个典型的金融行情数据单表7000多万行60多个字段。原始CSV大约28GB其中纯数字类型的字段占了总数据量的65%以上。光是把这些数字从文本改成二进制存储理论上就能砍掉接近一半的空间压缩之后直接掉到个位数GB。1.2 类型信息完全丢失CSV除了浪费空间还有个更致命的缺陷它不保存任何类型元数据。你打开CSV看到2024-03-15这到底是个字符串还是一个日期类型CSV本身完全不知道。读到1234567这到底是个整数、长整型还是字符串CSV也不知道。这就导致两个连锁问题。一是数据读取方每次都要做类型推断大批量读取时非常耗时而且推断还容易出错——比如某个字段大部分是数字但有个别行是空值或者异常文本推理引擎就会把这个字段当成字符串后续做聚合计算的时候还得先cast麻烦事一堆。二是压缩效率上不去。因为所有类型在CSV里都是字符压缩算法面对2024-03-15这种日期字符串虽然能查出点模式来但和二进制日期类型比如DuckDB/Parquet里的DATE类型只占4字节的压缩空间根本不是一个量级。这里多说一句日期2024-03-15在Parquet里存成DATE类型就是4个字节而CSV里的相同信息用了10个字节这是什么概念单这一个字段就省了60%的开销。所以说白了CSV把一份数据以最笨拙的方式摊开铺平30GB的原始数据里真正有价值的“信息”也许只有5GB剩下25GB都是在用文本形式硬扛。换个格式能缩到3GB道理就在这。2. 为什么是列式存储Parquet的降维打击换格式不是随便换你得懂自己要的是什么。市面上能选的格式不少JSON、JSONL、Avro、ORC、Parquet、HDF5、SQLite……但真要拿来做大数据集的长期存储和高效查询Parquet基本是当下最稳妥的选择。2.1 列式存储的真正优势Parquet是一种列式存储格式这一点是它区别于CSV的根本所在。CSV是行式存储一行数据完整地写在一起读文件的时候必须从头扫到尾。Parquet则把同一列的数据连续存放读某个字段的时候只读取对应的列块其他列可以直接跳过。列式存储对压缩有天然优势因为同列的数据类型一致数据分布也更集中。比如一个“用户等级”字段可能只有1到6六个值列式存储会把这一整段重复值放在一起压缩算法轻松就能把这段数据压到极小。但CSV是行式存储用户等级这个字段分散在文件的各个角落每遇到一次就要重新编码一次压缩率自然天差地别。我见过太多团队处理大数据时还在用CSV做存储其实只要换成Parquet很多时候连“上数仓”“搭集群”这些重活都省了。因为Parquet自带统计信息比如列的最大最小值、空值数量查询引擎可以靠这些统计信息直接跳过不需要的数据块这比傻扫CSV不知道快了多少倍。2.2 压缩算法怎么选Zstd是综合最优解Parquet本身只是个容器真正决定压缩比的是它内部的压缩算法。常见的有Snappy、Gzip、Zstd、LZ4几种各有优劣。我的实际经验是Snappy适合追求极速的场景Gzip压缩比最高但慢Zstd是日常使用最平衡的选择。以一批真实业务数据为例同样一份CSV转ParquetSnappy压缩后大约5.2GB耗时45秒Gzip压缩后大约3.1GB耗时4分半Zstd压缩级别3压缩后大约3.3GB耗时1分12秒。Zstd比Gzip只多了不到10%的空间速度却快了将近三倍妥妥的性价比之王。注意如果你准备把Parquet文件丢到Hive/Spark里跑SQL压缩算法的选择还要考虑集群的支持情况有的老集群只默认支持Snappy。不过单机分析场景就无所谓Zstd闭眼选。2.3 生态兼容性为什么Parquet能通吃一切Parquet另一个让我放心用的理由是生态太成熟了。Pandas、Polars、DuckDB、ClickHouse、Spark、Flink、Trino……只要你能叫得上名字的现代数据处理工具原生支持Parquet是标配。它背后是Apache社区的标准列式存储、谓词下推、向量化执行这些性能优化手段在你换到Parquet之后全部自动生效。还有个很多人忽视的点Parquet自带schema信息。你在转换时如果指定了字段类型以后任何程序读取这个文件拿到的都是明确的类型定义不会再出现“读CSV猜类型猜错”这种低级事故。对于要长期维护的数据集来说这一点比压缩省空间重要得多。3. 实操30GB的CSV怎么变成3GB的Parquet理论说再多不如直接动手。下面是我实际转换一个30GB CSV文件的完整过程用的工具是DuckDB。3.1 为什么用DuckDB而不是Pandas先把结论放这除非你的机器是512GB内存的主机否则别用Pandas读30GB的CSV。原因很简单Pandas执行pd.read_csv(file.csv)时会先把整个文件加载进内存再用一整套Python对象来表示DataFrame。30GB的CSV加载到内存里轻松占用80GB甚至更多普通人的电脑根本扛不住内存一爆就OOM进程直接被杀。DuckDB是走OLAP路线的分析型数据库它的理念就是“不需要把全部数据载入内存”。DuckDB读取CSV时采用流式处理内部有向量化执行引擎能够一边读一边压缩写出峰值内存占用可能只有几百MB。实测下来30GB的CSV转ParquetDuckDB跑完整个流程峰值内存连800MB都不到这是Pandas完全做不到的。如果你机器上还没装DuckDB可以用pip一行搞定pip install duckdb3.2 一条SQL完成核心转换DuckDB最爽的地方在于转换数据不需要写任何Python代码一条SQL就完事了。-- 从CSV读取数据直接写入Parquet COPY (SELECT * FROM read_csv_auto(data.csv)) TO data.parquet (FORMAT PARQUET, COMPRESSION ZSTD, ROW_GROUP_SIZE 100000);就这么一句话30GB的CSV就变成了3GB出头的Parquet。但这里有几个参数值得展开说说read_csv_autoDuckDB的自动类型推断函数它会扫描CSV的样本来自动判断每个字段的类型。如果不放心你也可以用read_csv并手动指定类型结构但绝大多数场景下auto模式已经足够靠谱。COMPRESSION ZSTD指定压缩算法我上面说过Zstd性能和压缩比最均衡。ROW_GROUP_SIZE 100000Parquet内部的行组大小默认也是100万左右。行组越大压缩率越好但随机读取某个小范围内的数据会慢一些。如果你的文件后续主要用于批量扫描保持大行组没问题如果经常要按条件过滤小部分数据建议适当调小比如10万。我在实际项目中数据量更大一点40GB也是用这条SQL跑的全程大概3分多钟出来的文件从40GB降到4.6GB压缩比接近9比1。这个效率Pandas跑同样的活可能要表崩溃好几轮。3.3 Pandas方案小数据量时怎么处理如果文件本身不大就1-2GB用Pandas转换其实也不是不行而且代码更直观。常见做法是分段读取import pandas as pd # 分段读取CSV每50万行一个chunk逐个追加写入Parquet CHUNK_SIZE 500000 writer None for chunk in pd.read_csv(data.csv, chunksizeCHUNK_SIZE): if writer is None: writer pd.io.parquet.ParquetWriter( data.parquet, enginepyarrow, compressionzstd, schemapyarrow.Schema.from_pandas(chunk) ) writer.write_table(pa.Table.from_pandas(chunk)) if writer: writer.close()这段代码的核心思路是不把全部数据塞进内存而是分块处理。每一块读进来马上转成Arrow表写入Parquet然后释放内存。理论上不管CSV多大都能跑但实际速度比DuckDB慢不少因为Pandas的chunk读取是按行切割内部还有大量Python对象转换开销。所以我的排兵布阵建议是文件大小推荐工具速度内存占用小于2GBPandas分段读取中等低2GB-20GBDuckDB快低大于20GBDuckDB 分区写出快极低3.4 转换后的数据校验别以为转换完就万事大吉了写完Parquet之后必须做数据校验这一步不能省。我的校验思路有三条行数对齐、数值抽样对比、类型检查。-- 校验行数是否一致 SELECT COUNT(*) FROM read_parquet(data.parquet); -- 查看最终Parquet的schema DESCRIBE SELECT * FROM read_parquet(data.parquet); -- 抽样对比若干条数据确认内容没丢没变 SELECT * FROM read_parquet(data.parquet) USING SAMPLE 100;行数对齐是最基本的CSV有多少行Parquet里必须也有多少行差一行都说明转换中途出了问题。数值抽样我一般用USING SAMPLE 100随机抽100行人工抽查看看关键字段有没有错位、乱码、精度丢失。类型检查主要是确认DuckDB推断的数据类型是否符合预期比如日期字段到底有没有被推断成DATE而不是VARCHAR。大坑提醒如果CSV文件里的某个日期字段有2024-02-30这种脏值DuckDB的auto推断可能会把整个字段推断成VARCHAR你的Parquet里那一列就变成字符串了查起来麻烦不少。遇到这种情况要么先清洗原始数据要么手动在read_csv里指定字段类型。3.5 更进一步多文件Parquet分区分区写入如果你有多个CSV文件要合并或者单个Parquet太大影响后续查询效率可以考虑分区写入。DuckDB也支持直接对分区字段做转换COPY (SELECT * FROM read_csv_auto(data_*.csv)) TO output_dir (FORMAT PARQUET, COMPRESSION ZSTD, PARTITION_BY (date_col));这样会在output_dir下按date_col的值生成多个子目录每个子目录里是对应分区数据的Parquet文件。查询时如果带了分区字段过滤DuckDB/Spark这类引擎可以直接跳过无关目录速度还能再上一个台阶。不过要注意分区字段本身不会重复存放在每个Parquet里你读出来的数据里会多一个隐藏的date_col列使用时别搞混。4. 踩坑实录与排查技巧转换文件这种事看起来就是一条SQL的事但实际跑起来各种各样的妖魔鬼怪全出来了。我把这几年遇到过的问题整理一下希望能帮你少走弯路。4.1 内存不够OOM到底怎么破用DuckDB都OOM这听着有点反直觉但确实遇到过。原因是这样DuckDB的read_csv_auto会先扫描文件的一部分样本来推断类型如果CSV文件行宽特别大比如上万个字段或者是某些特殊分隔符没识别对推断过程会消耗大量内存。另外如果CSV文件非常大而且你写的SQL里有排序、聚合这类需要全量数据才能算的操作DuckDB也会把中间结果往内存里塞。解决办法有几招加sample_size参数限制类型推断的扫描行数比如read_csv_auto(data.csv, sample_size10000)。在DuckDB里设置SET memory_limit4GB主动限制内存使用让多余的走外存避免整机卡死。如果单文件实在太大先按行拆分成几个小文件逐个转换最后再合并Parquet。拆分CSV其实没你想的那么复杂直接用Linux的split命令就行。但要注意CSV文件头文件不能被重复添加上去如果你用split按行数拆分第一个文件保留表头其余文件的表头得删掉否则转换时会出现大量类型推断错误。# 按100万行一个文件拆分先不保留表头 tail -n 2 data.csv | split -l 1000000 - part_ # 给拆分后的第一个文件补表头 head -n 1 data.csv part_aa cat part_aa.tmp part_aa # 这里建议用临时文件操作避免覆盖原文件实际操作时小心点别把原文件搞坏了我建议先复制一份再做拆分试验。4.2 类型推断失败脏数据引发的连环事故类型推断是CSV转Parquet里最容易被低估的坑而脏数据是罪魁祸首。最典型的情况是某个字段99.9%都是数字按理说应该推断成DOUBLE或者BIGINT但偏偏有几行的值是空字符串或unknownDuckDB一看有非数字内容直接把整列推断成VARCHAR。等你读完Parquet想算平均年龄、平均金额的时候发现全是字符串聚合函数都下不了手。处理方式我建议分两步走第一先做数据探查再决定怎么转。用DuckDB的SUMMARIZE函数可以快速看到每一列的类型分布和空值数量SUMMARIZE SELECT * FROM read_csv_auto(data.csv);第二对有问题的列手动指定类型。DuckDB的read_csv_auto支持类型覆盖COPY ( SELECT CAST(age AS INTEGER) AS age, CAST(amount AS DOUBLE) AS amount, date_col FROM read_csv_auto(data.csv) ) TO data.parquet (FORMAT PARQUET, COMPRESSION ZSTD);这样做有个前提你得先确认脏数据只是极少数并且能在cast时被DuckDB合理处理。如果脏值太多最好在源头清洗别指望转换的时候顺手搞定。4.3 编码问题CSV读出来全是乱码这个问题在中文环境里尤其常见。很多业务系统导出的CSV默认是GBK/GB2312编码而DuckDB和Pandas默认按UTF-8解析。一遇到GBK编码的CSV读出来的中文全是豆腐块和乱码。DuckDB本身不支持直接在read_csv_auto里指定编码所以面对GBK文件我的做法是先用iconv转编码再交给DuckDB处理# GBK转UTF-8输出临时文件 iconv -f GBK -t UTF-8 data.csv data_utf8.csv如果文件太大iconv直接整文件转码可能会有压力。可以先用head抽几行测试转码效果再决定是分块转还是全量转。文件一旦转成UTF-8后边的流程就顺畅了。4.4 输出Parquet的常见配置参数速查很多第一次用的朋友问我Parquet转换到底怎么调参数这里直接列一张速查表你照着抄就行。参数默认值推荐值适用场景COMPRESSIONSnappyZSTD追求压缩比与速度平衡ROW_GROUP_SIZE122880100000-1000000批量扫描用大值点查用小值PARTITION_BY无常用过滤字段需要按时间/地区等字段过滤时OVERWRITE_OR_IGNORE无OVERWRITE重复转换时覆盖旧文件这几个参数已经覆盖了我日常90%的需求不需要再深挖别的配置。5. 一点经验之谈数据存储这件事我一直信奉一个原则能存二进制就别存纯文本能让机器高效读就别让人眼舒服。CSV的最大优点是“打开就能看”但这也是它最致命的地方——它的一切设计都在为“人可读”服务完全牺牲了机器效率。换到Parquet之后副作用不止是省磁盘。文件小了拷贝传输就快了有列式存储和压缩查询响应速度也能上一个大台阶类型信息保留下游的ETL、BI报表都不再需要反复做数据类型清洗。你可能觉得换格式要花好几小时学新工具但以我的经验从CSV迁移到Parquet最耗时的反而是说服团队“我们真该换了”的过程真正动手转换的时间半小时顶天了。如果你手头也有一个躺了好几年、用了之后卡到不行的超大CSV我强烈建议你今天就花个十分钟跑一遍DuckDB的转换流程。等那个30GB的文件真的缩到3GB你回头再看当初各种“文件太大跑不动”的问题会发现很多难题压根不需要堆硬件换个思路就解决了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门