ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

30GB CSV秒变3GB Parquet:列式存储与压缩实战指南

30GB CSV秒变3GB Parquet:列式存储与压缩实战指南 朋友一直叫我维克干这行久了接触最多的就是各种乱七八糟的数据文件。上周有人丢给我一个30GB的CSV说是做年度流量分析用的让我先看看能不能跑得动。我盯着磁盘剩余空间沉默了几秒然后花十五分钟左右把整个文件换成了Parquet格式落盘只剩3GB。同样这批数据原来想查一个城市的汇总得等十几分钟现在秒级出结果。这篇就把整个思路、操作和踩过的坑彻底讲一遍。不管你是处理日志、报表导出还是整理大型数据集只要手上有超过几个GB的CSV这套“换格式”的玩法都值得参考。1. 30GB的CSV先算清楚这30GB到底装了什么1.1 文本存储的三笔“冤枉账”CSV本质上是一个纯文本文件里面所有东西都以字符形式存放。这意味着三个问题。第一数值类型被“写开了”。比如整数123456在二进制里用int32存只要4字节但CSV里要写6个字符等于6字节带小数比如0.123456789012345在二进制double里固定8字节CSV里却要16字节往上的文本而且长度还不固定。数值越精确这个浪费越明显。单条数据看着也就几十字节但一旦乘上千万级、亿级行数就是几十GB和几GB的差别。第二分隔符和换行符被重复存储。每一行都要有逗号分隔字段、换行符结束本行。这个开销是固定的但数据量一大就很可观。一亿行数据每行就算只有10个字段光逗号就是一亿个字符约100MB如果文件是Windows环境生成的换行是CRLF一亿行就是200MB。这些开销在二进制列式格式里都可以优化掉。第三重复的字符串只能反复写。比如几十亿条日志里某个城市名要出现无数次CSV每次都得原样写一遍换成二进制格式后字典编码可以把“Beijing”映射成整数0每行只存一个很小的整数。这一项在低基数字段上非常可观通常是压缩能到10:1的最大来源。这三笔账加在一起就能解释为什么30GB的CSV里装的“真实数据体积”可能远没有那么大。1.2 不同数据类型的体积对标一份数据能挤到多小我习惯在动手前先做一个“体积下界估算”避免对压缩结果产生不切实际的期望也方便判断压缩效率是否正常。假设一个表有4列用户ID字符串、城市名字符串、时间戳、消费金额小数。字段CSV中的表示大概字节数Parquet中的表示大概字节数用户IDu_1000000111字符串压缩6~11城市名Beijing7字典编码0.2~1时间戳2024-01-01 12:30:4519timestamp类型8消费金额123.456double类型8逗号和换行,\n2无需分隔符0这样一行在CSV里约45字节。如果表有5000万行CSV理论体积大约是45×5000万≈2.25GB。但同样的数据存成Parquet时间戳用时间类型存8字节城市名走字典编码后平均每个值可能不到1字节用户ID如果基数值高压缩空间有限整体体积能压到1GB左右。当然这只是估算模型真实文件里字符串长短不一、空值多、嵌套引号多实际结果会有波动。但方向是一致的30GB变3GB不是魔法是文本编码被换成了二进制编码顺便做了一次针对性的压缩。1.3 目标不是“压缩”是去掉冗余编码这里要特别澄清一个概念CSV直接打zip也能变小很多时候能压到30%甚至更低但那个方案的体验和Parquet完全不一样。zip是把整个文件当作一个大字符串流做压缩虽然体积变小了但你要分析数据时解压还是要等很久要随机取某一行得先把前面都解压完。也就是说zip只解决了“占空间”没有解决“读取慢、分析难受”。真正的目标应该是两层第一层是去掉文本格式带来的重复和类型浪费第二层是让压缩逻辑能够利用数据的结构特点。列式存储做的事情恰恰是这两件事一起做。所以公式不是“30GB压缩成3GB”而是“30GB里真正的信息量可能只有3GB格式转换只是把那27GB的文本容器去掉了”。这一点想明白后面选什么格式、用什么压缩算法、要不要保留原始CSV都会有更清晰的判断依据。对于归档、分析、传输场景二进制列式格式是比zip更优的答案如果你只是想把文件存起来永久不动那zip也够用但既然要做数据分析就别只想着“塞进柜子”还得想着“随时能翻出来用”。2. 方案选型换格式为什么选Parquet而不是压缩包2.1 CSV只是“交换格式”不是“存储格式”CSV之所以无处不在是因为它极简纯文本、无schema、任何编程语言都能读。但它也为此付出了代价——所有数据结构信息都被抹平类型、长度、重复关系全都丢掉了。用行话讲CSV是一种“交换格式”适合在不同系统之间传递数据不适合作为长期分析的基础存储格式。打个比方CSV像你出门旅行时随手拍的行李照片方便跟朋友描述带了多少东西Parquet像快递仓库里的标准化货架每一件货品都按编号、尺寸、存放位置登记。你说照片能不能当仓库用也能但找东西和搬运就很痛苦了。数据量到30GB这个量级每次读取都要全量解析文本CPU和内存都被大量浪费在“字符切分”和“类型转换”上。所以第一步选型不是“用哪个工具”而是“要不要继续把CSV当主力格式”。只要后续还会反复查询、聚合、抽样就应该换成二进制格式如果只是给别人交付一次对方也不一定需要高性能读取那保持CSV也可以。2.2 列式存储的压缩逻辑让同类型的数据待在一起Parquet最关键的设计是列式布局。普通行式存储比如CSV是按行把每条记录完整写在一起Parquet则是先把整个文件按行切成若干行组row group每个行组内部又按列分别存储。这个布局带来的好处非常直观同一列的数据类型相同值域相近压缩算法能发挥最大的威力。比如时间戳列全是时间类型连续两行的时间往往很接近用delta编码就能记录差值而不是完整值城市名列重复率高字典编码先把城市列表提取出来去重每一行只存一个字典下标。这些操作在CSV里是不可能做的因为文本流里数字、字母、符号混在一起压缩器只能看到一串没有结构意义的字符。列式布局还能带来查询时的“列裁剪”如果只要金额这一列扫描时根本不需要读其他列。这在30GB级别的大文件上效果比压缩本身还要明显。你做了格式转换后不只是少了27GB磁盘空间更是给后续的SQL查询装上了“只读需要的那块数据”的能力。2.3 Parquet、ORC、zip怎么选当下主流的列式二进制格式里Parquet和ORC是最常见的两个。ORC在Hive生态里非常流行很多OLAP场景都有优化Parquet背靠Apache Arrow和DuckDB和数据分析生态的结合更紧密Python、R、Spark、Polars、DuckDB都能直接读兼容性最好。格式核心特点适合场景上手门槛Parquet列式存储、生态广、支持字典编码和谓词下推数据分析、跨工具使用低pip安装pyarrow即可ORCHive生态友好压缩比高大数据平台Hadoop系偏高需适配环境zip/gzip CSV压缩率高但读取仍需全量解压冷数据归档最低Arrow/feather读写速度极快内存映射友好单机高频交互式分析低但压缩率一般个人处理CSV换格式的场景我推荐Parquet。原因很简单它不需要你额外搭一套Hadoop环境装个pyarrow或者duckdb就能开始学习成本和迁移成本最低。至于zip前面已经说了只能解决体积不能解决查询性能适合归档不适合分析。gzip也类似你把CSV压缩成csv.gz读取时还是要全量解压数据量大时依旧吃力。这里直接给结论如果目的是“存档”可以选gzip或者zstd压缩的CSV如果目的是“存档继续分析”选Parquet。2.4 压缩算法和分块参数从3GB再扣一点确定用Parquet之后还有两个参数需要做决定压缩算法和行组大小。Parquet支持的常见压缩算法有Snappy、Gzip、Zstd、LZ4、Brotli。在30GB这个量级我最推荐Zstd原因是压缩比直逼Gzip速度却快很多实测比Snappy多压出15%到30%速度损失却可以接受。如果对写入速度要求极高可以退而求其次用Snappy如果要追求极致压缩比且不介意时间长可以选Brotli或Gzip。行组大小是另一个关键参数。行组越大压缩率通常越高但读取单条记录时的随机读代价也越大行组越小查询裁剪越细但整体体积会略微增大。我建议用默认值128MB对大多数场景已经足够。如果查询模式经常是“按某个分区字段过滤”可以配合Hive分区目录把数据按日期或地区拆分到多个Parquet文件进一步缩小单次扫描量。这一步的取舍一句话总结不要为了“再压小一点”把查询速度牺牲掉。3GB这个结果本身就很好没必要为了2.8GB去等更久。3. 实操过程从30GB CSV到3GB Parquet的完整步骤3.1 转换前先给CSV做个体检拿到CSV之后不要直接闷头转换。先花五分钟做个体检确认文件编码、分隔符、表头、字段数量、行数、有没有空值和异常字符。这一步能避免后面白跑一遍。我常用的体检方式是先用文本工具看文件头部和尾部确认第一行是不是表头、分隔符到底是逗号还是分号。然后可以用DuckDB的read_csv_auto自动探测SELECT * FROM read_csv_auto(input.csv) LIMIT 5;如果这条语句能跑通且字段类型看着合理那就说明文件大概率可以直接转。如果报错通常要么是编码不对要么是某些行字段数不一致。此时再用Python的csv模块读几行把异常行打出来看看。另一个容易忽略的点是文件编码。中文CSV经常出现UTF-8和GBK混用的情况手机App通常会自动识别但命令行工具和数据库不一定。我的经验是先让DuckDB自动读乱码就用encoding参数指定编码SELECT * FROM read_csv_auto(input.csv, encodinggbk) LIMIT 5;编码确认对了再进入下一步。3.2 用DuckDB一行命令完成转换真到了转换环节我首选DuckDB因为它把CSV解析、类型推断、Parquet写入都封装好了一个COPY语句就能完成任务而且是流式处理30GB的文件不用一次性塞进内存。完整命令如下INSTALL parquet; LOAD parquet; COPY ( SELECT * FROM read_csv_auto(input.csv) ) TO output.parquet ( FORMAT parquet, COMPRESSION zstd );如果体检时发现分隔符不是逗号、字段类型需要指定可以改成这样COPY ( SELECT * FROM read_csv( input.csv, header true, delim ,, encoding utf-8, auto_detect true ) ) TO output.parquet ( FORMAT parquet, COMPRESSION zstd, ROW_GROUP_SIZE 122880 );这一步里read_csv_auto会扫描文件并推断出类型COPY的TO子句负责把结果写入Parquet。默认情况下DuckDB会利用多线程并行读取和写入处理30GB文件大概也就是几分钟到十几分钟的量级取决于磁盘速度。转换过程中DuckDB CLI下会看到进度条如果是在Python里调用duckdb可以直接执行并把结果打印出来确认没有报错。注意不要在转换命令上同时加ORDER BY或者复杂的JOIN除非确有必要。排序会拖慢写入速度而且Parquet本身不保证行顺序硬要排序会浪费大量时间。3.3 用pyarrow流式转换把内存占用压到最低如果你的环境不方便装DuckDB或者你想更精细地控制类型映射可以用pyarrow直接流式转换。这里最关键的是不要用pandas.read_csv一次性读入30GB大概率直接内存溢出。正确姿势是用pyarrow.csv.open_csv拿到RecordBatchReader然后分批写入ParquetWriter。示例代码如下import pyarrow as pa import pyarrow.csv as pv import pyarrow.parquet as pq # 流式读取CSVblock_size控制按块读取的字节数 reader pv.open_csv( input.csv, read_optionspv.ReadOptions(block_size64 * 1024 * 1024), parse_optionspv.ParseOptions(delimiter,), convert_optionspv.ConvertOptions(strings_can_be_nullTrue) ) # 用读取到的schema初始化ParquetWriter writer pq.ParquetWriter( output.parquet, schemareader.schema, compressionzstd ) for batch in reader: writer.write_batch(batch) writer.close()这段代码的好处是内存占用基本恒定不会因为文件是30GB就吃掉30GB内存。批处理大小可以通过block_size调整默认值很小我通常会调到64MB减少批次数同时单批内存压力也不大。如果你需要对特定列做类型修正比如把“user_id”从字符串变成整数、把“created_at”解析成时间戳可以自己在循环里对batch做cast或compute操作写完后再交给writer。类型修正这个步骤看起来简单实际是CSV转Parquet最容易出错的环节后面会单独讲。3.4 转换后如何验证文件没“变样”转换不是跑完就结束验证环节一定要做。最简单的验证有三步看文件大小、看行数、看抽样对比。看文件大小ls -lh input.csv output.parquet如果转换正常output.parquet应该远小于input.csv。如果没有明显缩小说明类型推断可能出了问题比如所有列都被读成了字符串字段没有利用字典编码和数值压缩。看行数-- DuckDB 直接查两个文件的行数 SELECT csv AS src, count(*) FROM read_csv_auto(input.csv) UNION ALL SELECT parquet, count(*) FROM output.parquet;行数一致说明没有丢行这是底线。抽样对比则更严格SELECT * FROM read_csv_auto(input.csv) USING SAMPLE 1000; SELECT * FROM output.parquet USING SAMPLE 1000;把两个抽样结果并排看确认关键字段内容一致。更高强度的校验可以用全外连接对比两边的行数/主键方法不止一种我在第4部分会专门展开讲MD5校验的做法和误区。注意对比行数的时候如果CSV里有空行或者末尾多了一个换行DuckDB对空行的处理策略可能会让行数差一这一步建议先在体检时确认CSV没有空行否则先清洗再转换。至此从CSV到Parquet的主流程就闭环了。30GB到3GB不是梦已验证。4. 常见问题与排查技巧实录4.1 转换时内存直接爆掉怎么办我见过不少人在这一步踩坑核心原因几乎都是用了pandas.read_csv读全量文件。30GB的CSV用read_csv读进来内存占用往往会飙到60GB以上因为pandas在解析时会生成多个中间副本。这时候机器直接卡死甚至OOM被系统杀掉。处理办法有三个按优先级排序第一选择用DuckDB的COPY它天然就是流式处理不需要手动分块。第二选择用pyarrow的open_csv ParquetWriter也就是上面那段代码内存恒定。第三选择如果你只能用pandas那至少要用chunksize分批读但pandas解析CSV本身开销大同样体积下比pyarrow慢不少不推荐在30GB量级硬撑。还有一个容易被忽略的点不是只有读CSV才占内存ParquetWriter在写入时也会缓存一定量的数据用于压缩。如果发现单批batch写完后内存并没有释放可以检查是不是把整个RecordBatchReader一次性list化或者collect了。正确做法是循环里处理一个batch丢一个batch不要攒着。4.2 中文乱码手机正常、电脑乱码到底是谁的锅很多人的CSV是从数据库或者平台导出的编码格式可能是UTF-8也可能是GBK。手机App普遍会自动识别编码所以看着很正常电脑上如果用Windows记事本或者老Excel打开可能默认按ANSI也就是GBK去解析UTF-8文件于是满屏乱码。这个问题和格式转换直接相关如果你用DuckDB读CSV时没有指定encoding默认会按UTF-8处理GBK文件就会被读出一堆乱码转出来的Parquet自然也是脏数据。所以体检阶段一定要确认编码可以用Python的chardet试试import chardet with open(input.csv, rb) as f: raw f.read(10000) print(chardet.detect(raw))但这方法不是绝对可靠大文件最好多截几段。更实用的办法是先尝试read_csv_auto看结果字段是否正常不正常就换成encodinggbk再看一眼。一旦确定了编码转换时固定写死不要让它自动猜避免后续偶发差异。转换完之后如果还有下游工具要求CSV你可以再从Parquet导出一份编码统一的CSV比如统一为UTF-8彻底解决手机和电脑打开不一致的问题。顺便提一句如果你在PyCharm里生成的CSV文件用PyCharm打开却不是一个表格这其实是正常现象。CSV本质是文本文件PyCharm默认用文本编辑器打开它不是表格软件。要想在IDE里像表格一样看需要装CSV插件或者右键选择“打开方式”里的Spreadsheet更直接的办法是用pandas、DuckDB这类工具去读而不是指望编辑器充当Excel。4.3 字段类型被猜错科学计数法、日期和长ID的坑CSV没有类型信息所以工具只能靠“猜”来推断每列类型。猜错的地方主要集中在三类长整型ID、日期格式、科学计数法。先说长ID。一个18位的订单号在Excel里经常会变成科学计数法比如1.23457E17后面的精度直接丢了在DuckDB里自动推断时也可能会把它读成DOUBLE而不是VARCHAR转成Parquet后精度立刻损失。处理办法在read_csv里显式指定该列为VARCHAR不要依赖自动推断。一旦丢失精度数据就没有回头路这一条比压缩比重要得多。日期格式的坑在于不同系统输出的格式太多2024-01-01、2024/01/01、20240101、01-JAN-24等等。DuckDB的自动推断一般能处理常见ISO格式但别指望它认识所有格式。最好的做法是先用LIMIT 5观察样例再在read_csv里统一指定DATE或者TIMESTAMP类型必要时用strptime转换。科学计数法的问题通常出现在浮点型字段比如经纬度、金额。CSV里写成1.23E-05自动推断成DOUBLE没问题但如果某些行是类似“1.23456E05”而另一行是普通小数工具也可能误判。稳妥起见数值列先按DOUBLE读进来再检查min/max和精度是否符合预期。4.4 转换后怎么校验数据一致性MD5应该怎么做很多人关心MD5校验但有个误区要先点破不要把MD5直接对CSV原文件和Parquet文件算因为文件格式不同二进制内容不可能相同MD5也必然不同。MD5校验在这种场景下不是用来比“文件是否相同”而是用来比“数据内容是否一致”。正确做法有两种。第一种存根法转换前先对CSV做一个“内容基线”的MD5。具体做法是把每一行按统一的规则规范化比如去掉行尾空格、统一分隔符、转成UTF-8然后逐行拼接成一个字符串流对整个流计算MD5。转换后把Parquet导出回CSV用同样的规范化规则再算一次MD5两个哈希应该一致。这个方法能准确发现“哪一行内容变了”但实现起来要注意很多细节比如空值怎么表示、Float的精度会不会因为二进制存储和文本转换产生误差。第二种数据库比对法更实在用DuckDB同时读CSV和Parquet对两张表做全外连接或GROUP BY对比。比如SELECT count(*) FROM ( SELECT * FROM read_csv_auto(input.csv) EXCEPT SELECT * FROM output.parquet );如果差集为空说明Parquet里的数据覆盖了CSV的全部内容。反向再查一次就能确认没有多行、少行、字段不一致。这种方式比MD5更直接唯一前提是两边schema要一致而你在转换时已经控制好类型了所以这个前提是成立的。MD5更适合用来做传输完整性校验比如文件从A机器传到B机器后确认字节没被改而数据转换的一致性校验交给SQL集合运算更高效也更不容易被格式化细节坑到。两者分工不同别混着用。4.5 换完格式后如何继续拆分、分析和导入数据库转成Parquet之后很多原本对CSV的操作习惯可以升级成更高效的SQL操作。比如拆分文件以前用CSV拆分工具很痛苦现在直接按条件导出COPY ( SELECT * FROM output.parquet WHERE date 2024-01-01 AND date 2024-02-01 ) TO 2024_01.parquet (FORMAT parquet);如果下游只认CSV也可以导出成CSV但建议还是尽量保持一致格式避免又绕回文本文件的坑里。往数据库导数据也一样。很多同学在DBeaver里导入CSV时经常遇到字段类型、分隔符的问题如果先把CSV转成Parquet再用支持Parquet导入的工具或者直接用DuckDB的SQL语法写回数据库整个流程会顺很多。像PostgreSQL可以先用COPY写CSV但如果你已经拿到Parquet了更常见的做法是先用DuckDB做查询和清洗最终只把结果表导出成小体积CSV再导入DBeaver或者数据库客户端。文件越小导入失败的概率越低。还有一些特定场景比如把示波器波形数据从CSV导进MATLAB做FFT分析、把航迹数据导入坐标转换工具等这类工具往往只接受CSV或TXT输入转成Parquet未必更方便。遇到这种场景我的建议是不要盲目追求转格式而是先用格式转换能力把“非必要的大文件变小”比如只保留需要的列、只保留目标时间段再导出回CSV给下游工具。转换格式是手段最终目标永远是“处理效率高、数据不丢”别本末倒置。4.6 常见问题速查表现象可能原因解决思路转换时内存爆掉pandas一次性读入CSV用DuckDB或pyarrow流式处理中文乱码编码识别错误显式指定encoding再转UTF-8长ID变成科学计数法类型被推断为浮点read_csv里指定为VARCHAR日期解析不对格式不标准先用LIMIT观察再strptime转换后体积没变小所有列被读成字符串检查schema按列指定类型MD5对不上直接对不同格式文件算MD5改成内容规范化的MD5或SQL集合差这张表基本覆盖了我处理大CSV转换时踩过的大部分坑。实际操作中还有什么特殊问题欢迎在评论区一起聊。最后再分享一个我实际处理时的习惯转完Parquet不要把原始CSV立刻删掉。保留原始文件一段时间作为备份确认后续所有查询、导入、校验都跑通了再考虑归档或删除。磁盘空间可以再买数据丢了可没有撤销键。而且30GB变3GB之后对比着看两个文件的读取速度、体积变化你会对格式转换这件事的理解更深一层。下次再有人拿大CSV来找你你心里就有底了。
返回列表