ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

30GB CSV转Parquet压缩至3GB:DuckDB实操与踩坑指南

30GB CSV转Parquet压缩至3GB:DuckDB实操与踩坑指南 如果你手头有一个 30GB 的 CSV 文件你通常的第一反应是头疼双击预览卡死Excel 直接拒绝打开Python 里 pandas 的read_csv大概率爆内存想发给别人还得传半天。这篇想和你聊的是我把这样一个 30GB 的 CSV 转成 Parquet 格式后只剩 3GB 的完整过程以及我在这个操作中实际踩过的坑、验证过的工具链和可以反复使用的一整套处理思路。无论你是做数据分析、后端报表还是平时要和大表格打交道的运维这篇文章都值得你花十分钟看完因为这类问题迟早会落在你头上。先说结论压缩比做到 10:1 并不夸张关键不是“压缩”这个动作而是“换格式”这个思路。30GB 的文本表格数据换成列式存储格式体积掉到 3GB 左右在日志、消费行为、航迹、运营明细这类数据上是非常常见的结果。你不需要改业务逻辑不需要写复杂脚本甚至不需要一台内存很大的服务器只要选对工具、理解数据特性、处理好类型推断和编码这几个关键点整个过程半小时内就能完成。下面我把这次实操从思路到细节完整拆给你看。1. 30GB 的 CSV 为什么换完只剩 3GB先算清这笔账很多人一听到“30GB 变成 3GB”第一反应是用了什么神奇的压缩算法。其实这里藏着两个完全不同的概念一个是压缩比一个是存储格式带来的结构性优化。CSV 和 Parquet 之间的差距不是简单的“压得更狠”而是存储方式的底层逻辑变了。1.1 行式文本的 CSV钱都花在哪了CSV 本质上是纯文本行式存储每一行代表一条记录每个字段靠逗号或者分号隔开。这种设计的好处是通用、可读性好任何一个文本编辑器都能打开所以至今仍是数据交换的默认格式。但它的代价也很明显。先说冗余。假设你有一张用户行为表里面有“省份”这一列一亿行里可能只有 30 多个不同值。CSV 会把“广东省”这个字符串原样写一亿遍每一遍都占着相同的字节数。如果是数字比如用户 ID、订单金额CSV 也会把它们变成字符串再存。数字 12345 在文件里占 5 个字符但用二进制整数存只需要 4 或 8 个字节。更不用提字段分隔符、换行符、引号这些额外的开销在十亿行级别的数据量下全是真金白银。类型信息也在 CSV 里完全丢失。一个文件里写着“2024-01-01”你光看文件本身分辨不出它是日期、是字符串还是什么自定义格式。下游系统读进来的时候必须再做一次类型推断而推断错了就是一堆脏数据。这就是为什么同一个 CSV 在手机上看是正常的、用电脑 Excel 打开却乱码——编码和应用场景的问题先不说CSV 本身就没有把“这列是什么类型”这件事写进文件里。1.2 列式存储是怎么把这几百斤水甩干的Parquet 是典型的列式存储格式。同样一张表CSV 一行一行地写Parquet 则是一列一列地攒。它会把某一列的所有值连续放在一起再进行编码和压缩。这个“连续放一起”带来一个直接好处同一列的数据类型统一取值分布规律更容易被压缩算法利用。举个例子省份列里有大量重复值。Parquet 在写文件时会先做字典编码把 30 多个不同的省份名各自映射成一个短编号后面每一行只需要存这个编号而不是完整的字符串。再结合压缩算法做二次压缩这列的体积从几百 MB 降到几 MB 都是正常的。数字列也一样整数用二进制存日期用时间戳存都比文本短得多。再加上 zstd、snappy 这类现代压缩算法的加持10:1 的压缩比在你数据重复度够高的时候真的不算极限。我见过有些日志类的 CSV 转完轻松压到 1/20。当然如果你的数据是高度随机的 UUID、毫无规律的哈希串压缩比会差一些但通常也能有 3~5 倍收益。这也是我强调“换格式”而非“加压缩”的原因gzip 能把 CSV 压到 3GB但读的时候依然要全量解压、依然没有类型信息、依然不能做列裁剪而 Parquet 在这 3GB 上还能提供谓词下推、列式扫描这些查询优化能力两者根本不是一个量级。2. 工具怎么选才不用自己造轮子几年前处理 30GB 的 CSV常见的路子是pandas.read_csv(chunksize...)分块读入再拼成 DataFrame 转存。这个方案不是不行但非常折磨人内存碎片化、类型推断不稳、跑着跑着分块逻辑就出 bug。而且如果你只是想换个格式没必要动用自己的应用服务器去扛这个压力。我用了几种主流方案实测对比了一下结论很明确优先选 DuckDB其次预计算引擎或 PyArrow 都可以真正能让你无脑抄作业的其实是两条命令。2.1 三个候选方案实测对比pandas、PyArrow、DuckDB先说 pandas。它的坑在于read_csv会把整个文件解析成内存里的对象30GB 的 CSV 光是读进来就可能吃掉 60~80GB 内存普通笔记本直接死机。用 chunksize 分块读又会遇到类型不一致的问题前十万行某列是整数到第一千二百万行突然出现一个点号pandas 分段解析出来的 schema 对不齐后面的活儿全乱套。用它做几十万行的小文件很顺手处理 30GB 场景绝对属于“能用但很难受”的级别不适合当主力方案。PyArrow 比 pandas 理性很多。它的pyarrow.csv.open_csv流式读取、pyarrow.parquet.write_table写入内存控制相当不错而且生成的 Parquet 质量很高。坏处是如果数据里面有复杂的嵌套结构、奇怪的编码或者跨行字段写起来要多处理好几个边界条件脚本越写越长。DuckDB 是我最后锁定的方案也是这次实操里真正跑通的工具。它是一个进程内的分析型数据库读 CSV 的时候会自动推断 schema流式读取几乎不占内存一条 SQL 就能把整个 CSV 写成一个 Parquet 文件。你不用自己管理 chunk、不用反复调试类型写 30 行的代码变成写 1 条 SQL。最重要的是它对不符合规范的数据有很强的容忍度后面我会专门聊它怎么处理各种脏数据。我用 5GB 的样本简单跑过一个对比方案内存占用预处理工作量转换耗时5GB 样本推荐指数pandas chunksize高峰约 12GB需要管分块与 schema约 6 分钟一般PyArrow峰合约 4GB需要手动写映射约 2 分钟推荐DuckDB峰合约 1.5GB几乎为零约 1.5 分钟强烈推荐注意这里的耗时和内存跟你本机配置、压缩级别有关但量级关系是稳定的。DuckDB 在性能、易用性、内存控制三方面都赢了这也是我把它作为本文主推工具的核心理由。2.2 为什么不是直接 gzip 压缩 CSV 就完事有人会问既然只是嫌文件太大那么gzip huge.csv之后也能得到 3GB 的.csv.gz是不是就完事了这是个合理的问题但实际用起来差别很大。.csv.gz最大的问题是“换汤不换药”。你虽然把文件体积压到了原来的 1/10但任何下游程序要读它都得先解压出完整 CSV再走一遍文本解析和类型推断流程。查询一个字段、过滤几行数据都得全量解压才行。而且 gzip 是流式压缩不支持随机访问想做并行处理非常困难。Parquet 文件则自带 schema、列式布局、压缩信息和统计信息。比如你想看某一天的数据Parquet 引擎会直接读取与日期列相关的 row group 和 column chunk其他数据根本不碰查询效率比解压整个 CSV 高好几个数量级。文件小了只是最表面的收益真正的价值是它把“读取”和“解析”的成本从每次重复消耗变成了文件本身的结构属性。以后不管用 DuckDB、Spark、pandas 还是数据仓库读这个 3GB 的 Parquet 都比读 30GB 的 CSV 快非常多。另一个细节是 gzip 之后你无法再有效分区或排序。Parquet 在写入时可以按指定列排序、按分区字段拆分目录之后查询时能直接跳过大量文件。CSV 即使你分成一堆小文件也还是需要在读取端做全量扫描和 schema 对齐。一句话总结gzip 只是给 CSV 穿了一件紧身衣Parquet 则是重新拆骨重组换了一套战斗装备。3. 实操30GB CSV 转 Parquet 的完整流程工具选好了接下来进入正题。我把整个流程分成三段转换前先体检转换中用 SQL 一把梭转换后必须做验证。很多人跳过了第一步和第三步结果转出来的文件要么类型全错要么行数对不上返工成本极高。这里的每一步都是我这次实操中真实走过的你可以直接对照着操作。3.1 转换前先做数据体检别拿全量文件当小白鼠拿到 30GB CSV 之后千万别直接上来就跑转换。我习惯先在命令行里抽几百万行看一眼字段结构确认三件事分隔符是什么、编码是 UTF-8 还是 GBK、有没有跨行的引号字段。DuckDB 的read_csv_auto能自动推断这些信息所以你不一定要手动看但最好先跑一个DESCRIBE确认 schema。用 DuckDB 的命令行工具或 Python 接口先探明结构duckdb -c DESCRIBE SELECT * FROM read_csv_auto(你的文件.csv, SAMPLE_SIZE1000000)SAMPLE_SIZE参数非常关键。DuckDB 默认采样 20480 行来推断类型如果这一百万行里恰好某列全是整数但后面某个位置突然冒出一个字符串默认采样就会把这一列错判成 BIGINT。你可以在read_csv_auto里把SAMPLE_SIZE调得大一点比如一百万或五百万这样类型推断的准确率会高很多。代价是预扫描时间变长但对 30GB 级别的文件来说多花一两分钟换一个不需要返工的类型结果绝对值。同时还要确认一下文件总行数和总列数。CSV 有表头吗表头是单行还是嵌套列数是否一致这些信息可以通过SELECT COUNT(*) FROM read_csv_auto(...)快速获得。不要小看这一步很多看起来“格式统一”的 CSV 到中间某一行会突然多出一个逗号导致列数错位DuckDB 默认会在strict_mode下报错但如果你没提前知道这个问题排查起来会非常痛苦。3.2 DuckDB 转换 SQL 与参数搭配体检没问题之后转换本身只需要一条COPY语句。下面是我这次操作的完整示例COPY ( SELECT * FROM read_csv_auto( /data/raw/behavior_log.csv, HEADER true, SAMPLE_SIZE 5000000, ALL_VARCHAR false ) ) TO /data/parquet/behavior_log.parquet (FORMAT PARQUET, COMPRESSION ZSTD, ROW_GROUP_SIZE 1000000);这里几个参数我具体说一下HEADER true表示第一行是表头如果 CSV 没有表头就设成false并手动指定列名。ALL_VARCHAR false让它自动推断类型而不是把所有列都当字符串。SAMPLE_SIZE 5000000是我前面提到的采样参数等于先扫描五百万行来定 schema。最后写入时COMPRESSION ZSTD是体积和压缩速度的较好平衡点如果追求更快的写入和读取速度可以用SNAPPY但体积会比 ZSTD 大一些。ROW_GROUP_SIZE决定 Parquet 内部行组的行数。默认 122880 行左右我调成 1000000 行主要是为了让行组更大对于后续按列扫描的类型会更友好。要注意的是这个值越大写文件时的内存缓冲也会相应增大DuckDB 默认内存限制是按需分配的不用太担心 30GB 文件把内存吃爆。如果你想按某个业务字段做分区比如按日期把数据拆成多个目录可以这样写COPY ( SELECT * FROM read_csv_auto(/data/raw/behavior_log.csv) ) TO /data/parquet/behavior_log (FORMAT PARQUET, PARTITION_BY (dt), COMPRESSION ZSTD);DuckDB 会生成dt2024-01-01/xxx.parquet这种目录结构后续查询如果指定 dt 条件就能直接跳过无关文件。这个功能非常实用但要清楚PARTITION_BY的列不会写进 Parquet 文件内部它是作为目录名存在的。如果你之后要把这些分区文件再读回 DuckDB依然需要通过目录名做过滤而不是在WHERE里直接筛那一列。3.3 转换后验证数量、类型、完整性一个都不能少转换完成不代表结束。我见过太多人跑完一条 COPY 就以为万事大吉结果下游 SQL 一查发现日期全变成了 NULL或者行数比原文件少了两万。所以验证这一步必须做而且最好做成脚本形式以后每次转换都能复用。验证分三层。第一层是行数一致性-- 原 CSV 的行数 SELECT COUNT(*) FROM read_csv_auto(/data/raw/behavior_log.csv); -- Parquet 的行数 SELECT COUNT(*) FROM /data/parquet/behavior_log.parquet;这两个数必须严格相等。如果 CSV 里有大量被引号包裹的换行字段一些工具会把换行当作记录分隔符行数对不上是最常见的翻车现场。第二层是类型检查。读 Parquet 之后执行DESCRIBE确认每个字段的类型与你预期一致。重点看日期、时间戳、数值、布尔这四个类型它们是 DuckDB 自动推断最容易出错的区域。比如原本应该是DATE的列被推断成VARCHAR虽然也能读但所有日期函数全都用不了数据还是脏的。遇到这种情况我的做法是在read_csv_auto里显式指定列类型不要让它猜SELECT * FROM read_csv_auto( /data/raw/behavior_log.csv, COLUMNS { event_time: TIMESTAMP, user_id: BIGINT, amount: DOUBLE } );第三层是抽样对比。不用全量查 CSV抽个几百行对比一下具体字段值就行。我习惯在转换前把 CSV 里某几行的关键字段打印出来转换后再从 Parquet 里抽查同样几行人工确认没有异常。另外如果你特别在意数据完整性可以在转换前给 CSV 计算一个 MD5 或 SHA 哈希值转换后对 Parquet 也计算同样的哈希DuckDB 里可以用MD5(encode(...))或读取后做聚合哈希两个值一致就说明内容未被破坏。不过要提醒你对 30GB 文件做全量哈希也是要花时间的建议只在关键数据或审计场景下使用。4. 实战中踩过的坑与排查心得写工具的人和用工具的人永远会被各种数据文件按在地上摩擦。转换过程中我遇到的坑远不止类型推断一个下面这几个是我筛选后觉得最有代表性的说出来能帮你少走不少路。4.1 类型推断打架日期字段被猜成 VARCHARID 被猜成 INTEGER前面提过SAMPLE_SIZE这里展开讲一个反面案例。我那次有一个 30GB 的用户消费记录其中一列是“用户注册日期”前几百万行全是2023-xx-xxDuckDB 用默认采样把它推断成了DATE。结果转换到后面某个位置突然有几十行数据是2023/xx/xx斜杠格式DuckDB 在 COPY 阶段直接报错提示无法将字符串转换为 DATE。这种问题有两种解法。第一种是把采样调大之前我用默认值确实漏了后来改成SAMPLE_SIZE5000000之后DuckDB 发现格式混用会自动把该列降级为VARCHAR。这不完美但至少转换不会失败。第二种是干脆在读取的时候就统一清洗用strptime把多格式日期全部转成标准DATESELECT *, strptime(注册日期, %Y-%m-%d) AS 注册日期_clean FROM read_csv_auto(/data/raw/behavior_log.csv);我建议如果你已经确定了业务标准格式直接采用第二种方案显式转换比让工具去猜更可靠。ID 列也有相似的问题某些 ID 超过 19 位数字会被推断成HUGEINT或VARCHAR看起来能用但后续 join 时对不上类型。我的经验是凡是业务主键类字段与其让工具推断不如直接指定VARCHAR省得因为精度问题导致关联失败。4.2 编码和分隔符手机打开正常电脑端乱码的真相再聊一个很容易被忽视的坑编码。CSV 文件里如果带着 UTF-8 BOMDuckDB 读字段名时第一个列名会多一个\ufeff前缀你在表里看到的是\ufeffuser_id而不是user_id。这种问题在命令行展示时不明显肉眼很难发现但下游程序拿这个列名去匹配时必然报错。手机打开正常、电脑打开乱码的问题本质上也是编码不一致。很多人在 Windows 上用 Excel 打开 CSV如果文件是 UTF-8 无 BOM 编码Excel 默认按 ANSI 或 GBK 去解码中文就会变成乱码。手机端 App 反而默认按 UTF-8 处理所以看起来正常。这不是数据坏了而是解码方式不对。解决办法很简单在转换前用编辑器或命令行工具把 CSV 统一转换成 UTF-8无 BOM编码或者如果数据以 GBK 为主就在 DuckDB 里指定编码再读。DuckDB 的read_csv_auto目前会自动检测 BOM但不会自动帮你转换 GBK遇到这种情况最好先用iconv转码iconv -f GBK -t UTF-8 original.csv converted.csv这里有个细节iconv 处理 30GB 文件会生成一个同样大小的临时文件所以磁盘要预留足够空间。或者你也可以用 Python 写流式转码逐行读写内存占用极小。总之编码统一之后再进 DuckDB可以避免很多莫名其妙的列名和内容乱码问题。分隔符也是一个隐形大坑。字段值里如果自带逗号比如地址字段“广东省,深圳市”CSV 里通常会加引号包裹。如果这个文件来自某些老系统引号包裹不规范DuckDB 自动推断时可能把一行拆成多行导致行数爆炸。遇到这种情况我推荐先用一个几十 MB 的小样本试运行而不是直接跑全量。4.3 内存不够的应急方案分批转换与并行度控制DuckDB 虽然内存控制很好但如果你是拿一台 8GB 内存的小服务器去处理 30GB 文件还是会紧张。我自己实测下来单纯一条 COPY 语句运行过程中内存曲线能压到 1~2GB但如果你同时开着好几个会话、或者系统里还有别的服务建议还是限定一下 DuckDB 的内存上限SET memory_limit 4GB; SET threads 4;threads控制并行线程数调低线程数会降低内存峰值但也会拉长转换时间。对于 30GB 文件4 线程通常能在几分钟内完成没有必要为了快而开满线程结果把内存吃满。如果内存实在不够还有最后一个杀手锏把大文件拆成多个小文件分别转换成 Parquet 再合并。注意不要用split直接按行数切因为如果文件里有跨行引号字段按行切会切坏。可以用 DuckDB 自己来拆COPY ( SELECT * FROM read_csv_auto(/data/raw/behavior_log.csv) WHERE id % 4 0 ) TO /data/parquet/part_0.parquet (FORMAT PARQUET);这种基于哈希的拆分能保证每个分片都完整不会切到半条记录。转换后再用 DuckDB 读所有 Parquet 文件放到同一张表里使用。这种做法的缺点是文件数量多、总大小可能略大于一次性转换的结果但对于内存受限的场景这是保命方案。4.4 下游工具兼容性转完格式后别让同事傻眼文件转完事情还没完。如果你是一个人处理数据那 Parquet 基本无脑合适pandas、PyArrow、Spark、DuckDB 都原生支持。但如果你所在团队还在用 Excel 或 DBeaver 直接处理 CSV就有必要考虑一下“转完格式后他人能不能用”的问题。DBeaver 导入 CSV 到数据库的特点是图形化、直观但处理 30GB 文件时也会非常吃力而且它并不擅长读 Parquet。如果你的下游工具是这类传统数据库客户端我更建议把转换后的 Parquet 作为中间层需要入库时直接通过 DuckDB 插入目标数据库而不是让同事手动去导入 CSV。比如INSTALL sqlite; LOAD sqlite; ATTACH output.db AS target (TYPE sqlite); COPY (SELECT * FROM /data/parquet/behavior_log.parquet) TO target.behavior_log;同样的方式也可以对接 PostgreSQL、MySQL。Parquet 作为分析层的核心存储CSV 只在数据交换边界保留原始版本这样既能保住 3GB 的小体积和查询性能也不会给同事增加额外门槛。另外一个常见的坑是 Excel 用户。Excel 到现在依然不能直接打开 Parquet所以如果你想给业务同事提供一份“能看”的摘要可以再从 Parquet 里导出一份几百 MB 的 CSV 或 Excel 给他们这比让他们面对 30GB 原始文件好太多。保留原始 CSV 的另一个好处是出问题的时候你可以回归原始文件复核Parquet 只是加速分析的工作副本。5. 一些个人操作习惯分享给你转换 30GB CSV 这件事我不太建议把它当作一次性脚本跑完就删。现实情况里这种大文件通常来自外部系统每周、每月都会有新的。我自己的习惯是把这个流程固定成一个可重复的.sql脚本放在数据目录旁边每次拿到新数据只要改文件名和输出目录就能复现。这样做最大的价值在于三个月后如果有人问你“这个 3GB 的 Parquet 是怎么生成的”你不用靠回忆直接把脚本丢给他顺便还能对比新旧数据的 schema 变化。还有一个经验是在正式处理 30GB 全量文件之前一定要先拿一个 1GB 左右的子集跑通整个流程包括类型推断、转换、验证三个环节。别嫌这一步多余很多坑在 1GB 的小文件上半小时就能暴露而在 30GB 全量上排查一次要等四十分钟甚至更久。我这次就是从 1GB 样本开始确认 schema、编码、分区字段都没问题之后才在夜间跑的全量任务。最后再分享一个小技巧。如果你后续还要经常查询这个 Parquet 文件可以在写入时对常用的过滤字段做ORDER BY。比如你经常按时间范围查数据写入时COPY ( SELECT * FROM read_csv_auto(/data/raw/behavior_log.csv) ORDER BY event_time ) TO /data/parquet/behavior_log.parquet (FORMAT PARQUET, COMPRESSION ZSTD);Parquet 文件内部会按 event_time 排序查询时配合 row group 统计信息DuckDB 可以跳过大量不含目标数据的块。这个操作对写入耗时影响很小但对后续查询性能的帮助是实打实的。处理大数据文件很多时候拼的不是机器多贵而是你愿不愿意花十分钟想清楚数据会被怎么用再花二十分钟把格式和结构设计好。这个习惯一旦养成后面省下的时间绝对不是一两顿饭的功夫。
返回列表