几百万行 CSV 不用起集群:用 DuckDB 在本地查明白
有次运营丢给我一个 800 万行的订单 CSV,要按城市统计个客单价。我兴冲冲 pd.read_csv,内存直接干到十几个 G,风扇狂转,最后还是 OOM 了。为了这一次的活去搭 Spark?不至于。装个数据库再把 CSV 导进去?光导数据就得等半天。
后来我用了 DuckDB,一行 pip install,直接对着 CSV 文件写 SQL,十几秒出结果,内存占用还很低。这篇讲讲它是什么、怎么用、以及边界在哪。

一、先说清楚它是个什么
DuckDB 是一个"嵌入式"的分析型数据库。 拆开讲:
- 嵌入式:它不是一个要你先启动、再连过去的数据库服务,而是一个库,一个文件。Python 里
import duckdb就直接能用,没有服务、没有端口、没有配置文件。这一点跟 SQLite 很像。 - 分析型:这里要说个词——OLAP(在线分析处理)。它适合的是"扫几百万行、按某列分组聚合"这种分析活儿,不适合"高并发地查单条记录、改单条记录"这种业务库活儿。SQLite 是嵌入式的事务型(OLTP)数据库,DuckDB 是嵌入式的分析型(OLAP)数据库,正好是两个方向。
它还有两个词常一起出现,顺手解释下:
- 列式存储:传统数据库按"行"存(一条记录的所有字段挨在一起),DuckDB 按"列"存(所有记录的同一个字段挨在一起)。你只查 3 个字段时,它就只读那 3 列的数据,其余的碰都不碰——这是它快的第一个原因。
- 向量化执行:CPU 处理数据不是一条一条来,而是一批一批(比如一次 2048 个值)批量算。这样能充分利用 CPU 缓存和 SIMD 指令,是它快的第二个原因。
二、装上就能用
pip install duckdb
然后直接对着文件写 SQL:
import duckdb
duckdb.sql("SELECT * FROM 'data.csv' LIMIT 5").show()
注意到了吗——没有建表、没有导入。FROM 'data.csv' 它自己会去读这个文件、自动识别表头和字段类型。
干点正经的活:
duckdb.sql("""
SELECT city,
count(*) AS cnt,
avg(amount) AS avg_amount
FROM 'orders_*.csv' -- 支持通配符,多文件一起读
WHERE created_at >= '2026-01-01'
GROUP BY city
ORDER BY cnt DESC
""").show()
orders_*.csv 这种写法特别省事,按月拆分的十几个文件一口气就查完了。
三、和 pandas 配合
已经有个 DataFrame 了,直接查:
import pandas as pd
import duckdb
df = pd.read_csv('small.csv')
result = duckdb.sql("SELECT city, sum(amount) AS total FROM df GROUP BY city").df()
df 这个变量名在 Python 里,DuckDB 能直接认出来当表用,不用注册。结尾的 .df() 是把结果转回 pandas DataFrame。
反过来,一个大 CSV 你想先用 SQL 过滤聚合、再交给 pandas 画图表,这么写:
df = duckdb.sql("""
SELECT * FROM 'orders.csv' WHERE amount > 100
""").df() # 结果小了,pandas 才扛得住
这是很实用的组合:让 DuckDB 干重活,让 pandas 干它擅长的数据整理和画图。
四、先把 CSV 转成 Parquet
Parquet 是一种列式的文件格式(可以理解为"为分析而生的 CSV")。同样的文件,Parquet 通常只有 CSV 的三分之一大小,查询还快好几倍,因为列式格式跟 DuckDB 的存储方式天然契合。
一次转换,之后反复用:
duckdb.sql("COPY (SELECT * FROM 'orders.csv') TO 'orders.parquet' (FORMAT PARQUET)")
之后就一直查 parquet 文件。如果你手上的 CSV 是常驻数据集,这一步的收益非常明显。
五、到底快在哪,边界在哪
跟几种常见选择对比一下:
| 场景 | pandas | DuckDB | Spark |
|---|---|---|---|
| 数据量 | 内存装得下才舒服 | 能处理远大于内存的数据 | 分布式,上 TB 级 |
| 上手成本 | 低 | 低,一个 pip | 高,要搭集群 |
| 表达方式 | Python 链式调用 | SQL | SQL / DataFrame |
| 部署 | 无 | 无,一个文件 | 要集群 |
它的边界要说清楚:DuckDB 是单机的。它能把单机性能压榨到极致,但你不能指望它横向扩展到几十台机器。数据量真到了 TB 级且持续增长,那还是得上分布式引擎。但对绝大多数"几百万到几亿行、在一台机器上"的分析任务,它比 Spark 轻太多了。
几个容易卡的坑
- 别把它当业务数据库用。它是分析场景设计的,并发写入支持很弱,事务也不是它的强项。别想着拿它替代 MySQL 存订单。
- 类型推断偶尔猜不准。CSV 没有类型信息,它靠采样猜。日期列可能被猜成字符串。发现不对就在查询里显式转换,或者先
CREATE TABLE指定类型再把数据导进去。 - 结果别全量转 pandas。
.df()会把所有结果读进内存。聚合完再转是常识,可真到写的时候经常有人SELECT *然后.df(),直接把内存打满,等于白用 DuckDB。 - 性能对比要公平。第一次查询慢是正常的,后面有缓存。另外它读 CSV 本身要花解析时间,转 Parquet 之后差异会大很多。
- 版本匹配。CLI 工具和 Python 包的版本要对齐,不然用 CLI 建的库文件 Python 可能读不了。
- 注意磁盘空间。它虽然能处理大于内存的数据,但中间结果会往磁盘落临时文件,磁盘满了照样失败。
小结
DuckDB 填的是一个很尴尬的空档:数据量大到 pandas 吃力,又没大到需要 Spark。装一个 pip 包,直接对着 CSV/Parquet 写 SQL,不用起服务、不用导入数据,几秒钟就能开始分析。我现在的习惯是:数据先转 Parquet,日常查询用 SQL,最后用 .df() 把聚合后的小结果交给 pandas 画图。整套流程都在一台笔记本上完成,不用再为了一次临时分析去申请资源。
- 点赞
- 收藏
- 关注作者
评论(0)