30GB CSV转Parquet压缩至3GB:DuckDB实操与踩坑指南
2026/9/15 4:34:32 网站建设 项目流程

如果你手头有一个 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_SIZE=1000000)"

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 会生成dt=2024-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 类型推断打架:日期字段被猜成 VARCHAR,ID 被猜成 INTEGER

前面提过SAMPLE_SIZE,这里展开讲一个反面案例。我那次有一个 30GB 的用户消费记录,其中一列是“用户注册日期”,前几百万行全是2023-xx-xx,DuckDB 用默认采样把它推断成了DATE。结果转换到后面某个位置,突然有几十行数据是2023/xx/xx,斜杠格式,DuckDB 在 COPY 阶段直接报错,提示无法将字符串转换为 DATE。

这种问题有两种解法。第一种是把采样调大,之前我用默认值确实漏了,后来改成SAMPLE_SIZE=5000000之后,DuckDB 发现格式混用,会自动把该列降级为VARCHAR。这不完美,但至少转换不会失败。第二种是干脆在读取的时候就统一清洗,用strptime把多格式日期全部转成标准DATE

SELECT *, strptime(注册日期, '%Y-%m-%d') AS 注册日期_clean FROM read_csv_auto('/data/raw/behavior_log.csv');

我建议如果你已经确定了业务标准格式,直接采用第二种方案,显式转换比让工具去猜更可靠。ID 列也有相似的问题,某些 ID 超过 19 位数字会被推断成HUGEINTVARCHAR,看起来能用但后续 join 时对不上类型。我的经验是:凡是业务主键类字段,与其让工具推断,不如直接指定VARCHAR,省得因为精度问题导致关联失败。

4.2 编码和分隔符:手机打开正常电脑端乱码的真相

再聊一个很容易被忽视的坑:编码。CSV 文件里如果带着 UTF-8 BOM,DuckDB 读字段名时第一个列名会多一个\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 可以跳过大量不含目标数据的块。这个操作对写入耗时影响很小,但对后续查询性能的帮助是实打实的。处理大数据文件,很多时候拼的不是机器多贵,而是你愿不愿意花十分钟想清楚数据会被怎么用,再花二十分钟把格式和结构设计好。这个习惯一旦养成,后面省下的时间绝对不是一两顿饭的功夫。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询