资讯动态

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

发布时间:2026/9/14 19:55:27 来源:尧图企业网站定制
收到一份 30GB 的 CSV 时我的第一反应不是“这文件里有多少数据”而是“打开它得先换个电脑”。当时我把文件丢进 Excel看它转了五分钟还在转圈然后就放弃了。接着用 Pandas 去读等了十几分钟直接抛了个MemoryError那一刻真的有点想砸机器。这种超大 CSV 大多出现在这些场景数据库导出的历史订单、IoT 设备的采集日志、第三方系统提供的批量数据或者爬虫攒了几年的原始数据。它们的共同点是行数多、口径杂、谁都不想给你接口最后给个 CSV 让你自己处理。问题在于CSV 这个格式本身就是为了“让人能打开看看”设计的不是给程序高效读取设计的。30GB 只是个开关真正麻烦的是它背后那串连锁反应。1. 30GB的CSV真正让人崩溃的不是“大”1.1 文件大只是表象真正的问题是“文本”占满了所有环节CSV 里存的全是文本。数字是文本日期是文本时间也是文本连一个空值都可能写成四个字符的NULL。我们平时觉得几十 KB 的 CSV 没问题那是因为文本的可读性确实强一旦数据量上来文本存储的劣势就完全暴露了。举个例子一个整数 123456 在 CSV 里要占 6 个字节一个字符一个字节但在内存里用 int32 表示只要 4 个字节用 int64 是 8 个字节。看似差距不大但考虑到 CSV 还要加分隔符、换行符以及日期时间这种动辄十几个字符的文本整体占用膨胀到 2 到 5 倍很常见。更关键的是程序读 CSV 时要把这些文本一个个解析成对应的数字、日期、布尔值这个过程非常耗 CPU。所以 30GB 的 CSV 加载进内存实际占用往往超过 60GB再加上解析时间普通笔记本根本扛不住。1.2 加载比同体积二进制慢几十倍卡死不是偶然有一次我做个小实验同样一份数据一份导出成 CSV30GB一份用列式存储导出压缩后只有 3GB 左右。用脚本读取 CSV 全量加载耗时约 4 分钟内存峰值 60 多 GB最后还差点 OOM读取列式格式只花了不到 30 秒内存峰值不到 5GB。这差距不是“快一点点”是数量级上的碾压。原因在解析逻辑上。CSV 是逐行逐字段处理的先按换行符切块再按分隔符切列然后对每个字段做类型推断、字符串转换遇到引号包裹的字段还得处理转义。这一整套流程下来CPU 一直在忙内存一直在涨。二进制格式就简单得多按固定类型和长度直接读入几乎不需要解析。所以我想明白了一件事面对 30GB 的 CSV纠结内存不够、电脑不行方向就错了。该换的不是机器是存储格式。2. 换格式省90%空间的底层逻辑文本变列式标题里说“换个格式只要 3GB”有人可能会觉得夸张。实际上只要你选的格式对压缩到十分之一是很常见的结果甚至某些数据能压到 1GB 以内。核心原因就一句话把“给人看的文本”换成“给机器算的列式二进制”。CSV 是行式存储一条记录的所有字段物理上挨在一起。Parquet 这类格式是列式存储一列的数据物理上放在一起。别看只是排布方式变了它数据结构上多了两个杀手锏列的类型化存储和列级压缩编码。2.1 一个具体的例子1000 万行订单数据怎么省下来的假设有一张订单表字段是订单号、用户 ID、金额、状态、创建时间。CSV 里一行大概是这样的A100001, U888, 128.50, SUCCESS, 2024-01-15 10:03:21保存下来一条记录占 40 多个字节1000 万行就是 400MB 左右。换成 Parquet 之后“金额”这一列会统一转成 float64 或 decimal“状态”这种取值只有几种的字段会自动做字典编码把“SUCCESS”“FAILED”“PENDING”这些重复字符串映射成短整数“用户 ID”这种重复率高的列也会走压缩。结果就是同样 1000 万行数据Parquet 只剩 60MB 上下。数值列还有更进一步的压缩手段比如行程编码、位打包。一个交易金额字段如果波动不大zstd 压缩后体积甚至可以再砍一半。30GB 的 CSV 转成 Parquet压缩后落到 3GB 附近是非常正常的不用觉得不可思议。2.2 Parquet、Feather、ORC怎么选适合当“终极搬运工”的格式有这么几个Parquet、FeatherArrow IPC、ORC还有 SQLite 或自定义二进制。我实际对比过简单结论如下格式压缩率读取速度生态支持适用场景CSV无压缩慢谁都能读交付给不懂技术的人Parquet高快极广Spark、DuckDB、Pandas、Polars数据分析、数仓、长期存储Feather中最快Arrow 生态程序内部临时交换ORC最高快Hadoop 生态为主离线数仓这里首推 Parquet。它几乎成了现代数据生态的通用语言DuckDB 能直接读Pandas/Polars 通过 PyArrow 也能读Spark 就更不用说了连各种 BI 工具都支持。Feather 虽然读取速度更快但压缩率不如 Parquet生态也没那么广。ORC 压缩率最高但和 Hadoop 绑定太深非 Hive 环境用起来折腾。相比之下Parquet 是那个“不会出错”的选择。有读者可能会问用什么工具转如果只是小文件Pandas 一条to_parquet就完事。但 30GB 的 CSVPandas 直接读就会把内存打爆所以下面我用 DuckDB 和 Polars 两条路线分别讲实际操作。3. 实操DuckDB 一条命令完成全量转换DuckDB 是我这几年处理大 CSV 最顺手的工具没有之一。它是个嵌入式分析型数据库单文件运行不需要装服务端对超大 CSV 的支持做得非常强内存不足时还能自动落盘。转换 30GB CSV 到 Parquet只需要在 DuckDB 里执行一条COPY命令。3.1 环境准备装 DuckDB 命令行工具去 DuckDB 官网下载对应平台的命令行版本只有一个可执行文件解压就能用。我这里用的是 Linux 环境文件名叫duckdb直接放到/usr/local/bin或者当前目录duckdb --version能输出版本号就说明装好了。DuckDB 不需要额外配置启动时指定一个数据库文件名或者用:memory:走内存模式都行。因为我们要做的是一次性转换用内存模式就够了duckdb :memory:进入交互式命令行后直接输入 SQL。Parquet 读取和写入功能是 DuckDB 内置的不用INSTALL任何扩展。新版 DuckDB 里read_csv会自动推断表结构和类型省掉很多手动步骤。3.2 一条 COPY 命令完成转换假设 CSV 文件叫orders.csv列名在第一行字段用逗号分隔。在 DuckDB 交互式命令行里执行COPY (SELECT * FROM read_csv(orders.csv)) TO orders.parquet (FORMAT PARQUET, COMPRESSION ZSTD);这条命令的意思很简单用read_csv自动探测并读取整个 CSV然后把结果输出成 Parquet 文件压缩算法用 zstd。read_csv默认会尝试自动识别列名、分隔符和数据类型对于大多数正常导出的 CSV 都够用。如果 CSV 文件没有表头需要指定headerfalse手动给列起名字比如COPY ( SELECT column0 AS id, column1 AS user_id, column2 AS amount FROM read_csv(orders.csv, headerfalse, delim,) ) TO orders.parquet (FORMAT PARQUET, COMPRESSION ZSTD);更大的文件我建议不放在交互式命令行里跑而是写成 SQL 脚本文件然后后台执行。比如把命令存成convert.sql再用duckdb :memory: convert.sql这样一个命令搞定即使会话断了任务也已经交给系统进程不依赖终端窗口。实测这次 30GB 文件转换前端用了不到 30 秒就把 CSV 扫完建好 schema后续写入持续了大约 18 分钟峰值内存也就 3GB 多比 Pandas 直接读少了一个数量级。3.3 如果更习惯 Python用 Polars 流式写 Parquet有些人可能不想学 DuckDB 的 SQL更希望在 Python 里一条龙处理。Polars 的scan_csv配合sink_parquet是最理想的方式它走的是惰性查询引擎不会把所有数据一次性加载进内存import polars as pl pl.scan_csv(orders.csv).sink_parquet(orders.parquet, compressionzstd)注意这里是scan_csv不是read_csv。scan_csv只建立查询计划不真正读数据sink_parquet会把计划流式执行并直接写出 Parquet 文件内存占用很低。如果你用read_csv再去to_parquet那把 30GB 读进内存又有 OOM 风险等于白折腾。如果有特殊列类型需要指定可以在scan_csv里传schema_overrides它等价于 DuckDB 里手动设置列类型。其余情况默认推断已经够用。Polars 这条路我也测过30GB 文件全程内存占用约 6GB耗时约 22 分钟比 DuckDB 稍慢但也在可接受范围。3.4 实测数据不是玄学是真的省了 91%转完之后我看了下结果输入orders.csv30.2GB输出orders.parquet2.8GB压缩率约 90.7%。这是没做任何手工优化、靠默认 zstd 压缩拿到的结果。如果数据里重复字符串多、数值集中在某个区间压缩率还能更高。有个细节值得提一下COMPRESSION ZSTD后面还可以接压缩级别参数比如ZSTD(LEVEL 19)压得更小但写得更慢。转换是一次性的如果你对体积特别敏感可以试着调高等级如果追求速度默认级别就行。磁盘空间足够的话我个人是默认级别拉满速度后边再考虑分区。4. 转换完怎么用这3G数据的读取速度和资源占用很多人以为转格式就是为了“存起来占地方小”其实省空间只是副产品。真正的收益在转换之后的使用环节。4.1 在 DuckDB 里直接对 Parquet 做聚合查询转成 Parquet 之后DuckDB 支持直接把它当表查不用再导回 CSVSELECT date_trunc(month, created_at) AS month, count(*) AS cnt, sum(amount) AS total_amount FROM orders.parquet WHERE status SUCCESS GROUP BY 1 ORDER BY 1;注意这里FROM后面直接写了一个orders.parquet文件路径DuckDB 原生支持读 Parquet连加载步骤都省了。查询执行时DuckDB 只会读取created_at、status、amount这几列其他列完全不碰。这就是列式存储的好处列裁剪让扫描量变得极小。实测用这条 SQL 扫 30GB 数据转换出的 2.8GB Parquet耗时不到 3 秒。同样的事如果对 CSV 来做首先得把 30GB 整个读进来哪怕最后只算一个求和也逃不过解析所有文本的代价。换了格式后这一步的时间成本几乎可以忽略。4.2 用 PyArrow 按需把数据拉进内存做分析如果你还是想在 Python 里做分析PyArrow 是最好的桥梁。它把 Parquet 读进内存时支持列裁剪和行过滤能做到“只读我要的那部分”import pyarrow.parquet as pq table pq.read_table( orders.parquet, columns[user_id, amount, created_at], filters[(amount, , 100)] )这段代码只会从文件里读取user_id、amount、created_at三列并且只保留金额大于 100 的行。底层原理是 PyArrow 会把下推条件传给 Parquet 文件读取器直接跳过无关的数据块。如果你的过滤条件落在被分区或排序过的列上效率还会再高一个量级。Pandas 用户也不用担心df table.to_pandas()就能转成 DataFrame 继续干活只是这时候内存里已有的数据已经很少了不会像直接读 30GB CSV 那样爆炸。4.3 后续能做的事分区、增量转换、临时导出Parquet 文件不是只能做成一个。如果 CSV 是按天导出的你可以转成按月份分区的目录结构比如COPY ( SELECT * FROM read_csv(orders.csv) ) TO orders_partitioned/ (FORMAT PARQUET, COMPRESSION ZSTD, PARTITION_BY month);分区之后查询某个 3 个月前的数据DuckDB 可以跳过完全无关的分区目录速度还能再上一个台阶。增量更新也一样新来的 CSV 转成新的 Parquet 文件放进同一目录查询时 DuckDB 会把多个 Parquet 当作一张表去扫旧文件和你拼出来的数据逻辑上是连续的。至于临时要看某几条数据也不用再拖回 ExcelCSV 本身作为“给人看的界面”保留一份小的就行。大文件一律走 Parquet日常工作里的分析痛点基本就解决了。5. 转换前的检查清单这些坑我都踩过转换看起来就一条命令但实际操作时坑不少。我按踩坑频率排个序挨个说一下。5.1 表头与无表头读错了整列数据都歪了如果是系统导出的 CSV多数带表头read_csv默认会把它当成列名。但有些导出的 CSV 前面会带几行说明文字比如“导出时间2024-01-01”这种这时候就得用skip2跳过前两行。最稳的做法是先read_csv后再SELECT * FROM read_csv(...) LIMIT 5看一下长什么样确认列名和分隔符对不对再跑转换。5.2 类型推断失败身份证号不能按数字存类型推断是自动的但自动不代表准确。比如身份证号、订单号这类“看起来是数字”的列CSV 里没加引号时DuckDB 和 Polars 会默认推断成 bigint一转换就把前导零丢了数据直接废掉。我的做法是凡是业务上不参与计算的号段字段一律在schema_overrides或手动CAST里指定成 VARCHAR。以 DuckDB 为例COPY ( SELECT CAST(id_card AS VARCHAR) AS id_card, amount FROM read_csv(orders.csv) ) TO orders.parquet (FORMAT PARQUET, COMPRESSION ZSTD);另外极少数列会有混合类型比如某个字段大部分是数字但某些行是“N/A”。DuckDB 在这种情况可能会推断成 VARCHAR导致数值列没法直接聚合。这种情况要么在转换前清洗要么转到 Parquet 后用TRY_CAST处理不要指望格式转换自动解决脏数据。5.3 编码问题中文 CSV 最容易翻车中文环境导出的 CSV经常是 GBK 或 GB18030 编码而 DuckDB、Polars 默认按 UTF-8 处理。30GB 的 GBK 文件直接读报错或者乱码都算小的有时候会导致列数对不上、数据丢失。我这边遇到过几次最后都是用iconv先转编码再进 DuckDBiconv -f GBK -t UTF-8 orders.csv orders_utf8.csv这一步需要双倍磁盘空间转换也要花些时间但它是必需品。如果你的 CSV 来源不固定建议写个脚本自动探测编码别硬猜。也可以在 Python 里用chardet或者charset-normalizer跑一下再决定虽然对 30GB 的大文件跑全量编码探测不现实但抽前几千行判断已经足够。5.4 分隔符和引号看起来是 CSV其实是 TSVCSV 名字里有逗号但实际导出时可能是分号、Tab甚至是竖线分隔。尤其某些老系统导出的文件字段值里还带换行和英文逗号如果不告诉解析器用引号包裹转换结果就会错行乱列。这些判断我一般都在转之前用 DuckDB 的read_csv参数显式指定SELECT * FROM read_csv(orders.csv, delim;, quote, headertrue) LIMIT 10;顺便说一句CSV 文件里的字段如果有换行普通cat工具看会以为行数变多了不用慌解析器认引号就行。转格式后这类文件同样没问题Parquet 是不关心字段内部换行的。5.5 转换完成后的校验别急着删原文件我吃过一次亏转换完直接删了原 CSV后来发现某列类型推断错了数据已经写进 Parquet想改还得重新导。从那以后我的习惯是转完后至少抽查几组聚合结果和原 CSV 做对比确认没有偏差再清理源文件。比如算一下行数和金额合计SELECT count(*), sum(amount) FROM orders.parquet;再用命令行快速看下 CSV 的行数wc -l orders.csv数字对上了再考虑删。要是总行数都对不上那前面某个坑大概率踩中了趁原文件还在赶紧排查。转格式这件事本质是把“给人看的数据”变成“给机器算的数据”。30GB 压缩到 3GB 只是第一步后面查询从分钟级变成秒级才是真正的收益。如果你现在正被一个大 CSV 卡住建议直接照上面的步骤跑一遍 DuckDB 的 COPY 命令先解决工具问题再解决数据问题。我自己现在收到 CSV 的第一件事永远是转 Parquet这个习惯帮我省下的不只是磁盘还有大量等待时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价