先说结论:一台 16G 内存的 ThinkPad X1(2021 款,i7-1165G7,四核八线程)能干完 58.7GB CSV 的活,前提是你别用 Excel,也别一上来就想着上集群。上周五下午三点,运营把一个 58.7GB 的 CSV 甩到我微信上,文件名还叫「订单明细_最终_最终版.csv」,125,846,321 行。我用 Excel 点开,往下滚,滚到第 1,048,576 行就到底了,右下角那个加载圈转得我心慌。这是 Excel 的天花板,多一行都塞不下。那天我盯着那个圈转了大概四分钟,然后它崩了,一点提示都没有。
为什么我第一反应不是上 Spark?公司有集群,YARN 队列叫 root.prod.etl,排队 3 小时起步。上个月我跑一个 2.1GB 的 join,从下午 2 点等到 6 点 17 分才出结果,中间被两个优先级更高的任务插队。我对这套东西是有偏见的——「大数据」这三个字,有时候是拿来撑场面的,不是拿来解决问题的。那天我脖子僵得厉害,后颈贴了两片膏药,实在不想再跟调度器斗智斗勇。所以我就想在自己这台笔记本上把它干了。
第一步:装 DuckDB,先把内存和线程按住
pip install duckdb==1.1.3,17 秒装完,比等一杯瑞幸还快。但别急着读文件,先把内存、线程、临时目录三样东西设好,不然它默认吃满你所有内存,然后被系统 OOM Killer 干掉——我第一个晚上就是这么白熬的。
import duckdb
con = duckdb.connect('/data/work/mydb.duckdb')
con.execute("SET memory_limit='10GB'")
con.execute("SET threads=6")
con.execute("SET temp_directory='/data/tmp'")
那个 temp_directory 我一开始没设,默认写到了 /tmp,而 /tmp 挂在一个 20GB 的根分区上。跑到 40 分钟左右报错「No space left on device」,我整个人是懵的。后来把临时目录指到一块 1TB 的 NVMe 上,才顺过去。线程数也别贪,四核八线程设 6 就够了,设 8 反而互相抢,实测反而慢 8% 左右。
第二步:读 CSV,这里有个能吃你两小时的坑
直接 read_csv_auto 是能读,但默认 sample_size 只有 20480。我这张表里有个 order_no 列,前两万行全是纯数字,DuckDB 就把它推断成 BIGINT 了,结果读到第 60 万行左右突然冒出 'A1023' 这种带字母的,直接抛错。我在这上面卡了整整两个小时,来回改 SQL,最后才发现是采样的问题。
CREATE TABLE orders AS
SELECT * FROM read_csv_auto('/data/orders.csv',
header=true,
sample_size=500000,
ignore_errors=true,
parallel=true);
58.7GB 读进来、建表,耗时 19 分 42 秒。中途风扇声音大到我以为它要起飞,键盘左上角烫手,我拿了个冰可乐罐垫在掌托边上——这招不推荐,后来冷凝水把 D 面弄湿了一小块。
第三步:转 Parquet,这是全场性价比最高的一步
COPY orders TO '/data/parquet/orders/' (
FORMAT PARQUET,
COMPRESSION ZSTD,
ROW_GROUP_SIZE 122880,
PARTITION_BY (dt)
);
58.7GB 的 CSV,转完 11.2GB,压缩比大概 5.2:1。同样的一个 group by 查询,直接扫 CSV 要 47 秒,扫 Parquet 只要 3.4 秒,快了 13 倍多。分区字段我用了 dt(日期),按天分成 412 个目录。这里有个教训:小文件太多反而更慢,所以 ROW_GROUP_SIZE 我给到了 122880,别再小。
第四步:我遇到的「类数据倾斜」,以及一个很脏的解法
单机的 DuckDB 其实没有 HDFS 那种分区倾斜,但 group by 一个超大 key 的时候,一样会卡。我这张表里 SKU-00417 这一个商品占了 31.2% 的行数,大概 3900 万行。GROUP BY sku 的时候,6 个线程里有 5 个早早跑完了在干等,就剩一个线程啃这个大 key,整体耗时从 3.4 秒涨到了 21 秒。我的解法很脏:
CREATE TABLE part_big AS
SELECT sku, count(*) c FROM orders WHERE sku='SKU-00417' GROUP BY sku;
CREATE TABLE part_small AS
SELECT sku, count(*) c FROM orders WHERE sku<>'SKU-00417' GROUP BY sku;
SELECT * FROM part_big UNION ALL SELECT * FROM part_small;
硬编码一个 SKU 值,写死在 SQL 里,非常丑陋,明天换个爆款就失效。但那天晚上 11 点 40 分,它把总耗时压回了 4.1 秒。我没打算改。真的,很多时候能跑的脏 SQL 比漂亮的分层建模有用得多,我不太信「一次写好永流传」这套。
最后几句实话
凌晨 1 点 17 分,我把结果导出成 8 个 CSV,每个控制在 800MB 上下,因为运营那边的 Excel 最多也就撑到 100 万行出头。手上全是薯片渣,眼睛干得像砂纸。这套流程总共花了我 6 小时 20 分,如果那天我真的去排 Spark 队列,可能到第二天早上都没跑完。
我不觉得 DuckDB 能取代集群,几 TB 以上的数据照样得分布式。但对日常 80% 的「中等数据」需求——几十 GB 到一两百 GB——单机工具就是比集群快且省事。所谓大数据,很多时候是数据不大、架子很大。这话有点偏激,我知道,但我照着做了半年,没后悔。