DuckDB 实战:SQL 直查 CSV 与 Parquet

做数据分析时,最常遇到的场景不是“搭一套数据仓库”,而是“手头有几十个 CSV 和一个几 GB 的 Parquet,只想用 SQL 快速聚合一下”。DuckDB 就是为这种需求而生的嵌入式分析引擎:它把 OLAP 能力塞进一个进程内引擎,不用起服务、不用建表,直接对本地文件跑 SQL。本文用可复用的命令,带你把 DuckDB 接入日常分析流。

为什么需要 DuckDB

传统做法要么用 pandas 把全量读进内存(容易 OOM),要么把数据导进 MySQL 再查(链路重)。DuckDB 走中间路线:列式向量化执行、对 Parquet/CSV 做谓词下推与分区剪枝,单机就能跑上亿行聚合,且内存可控。它还能直接把已有的 MySQL 表 当外表,把“库内数据 + 本地文件”一气呵成地 JOIN。对想做轻量分析、又不想说服运维开数仓的同学,它是性价比最高的起点。

和 pandas 相比,DuckDB 的计算发生在 C++ 向量化引擎里,避免了 Python 解释器逐行遍历的开销;和 Polars 相比,它更强调“直接吃文件”、零拷贝读取 Parquet 与 CSV,且兼容标准 SQL,迁移成本最低;和 Spark 这类分布式引擎相比,它不需要集群与运维,单机就能覆盖绝大多数分析师的日常。换句话说,DuckDB 占据的是“一个人、一台笔记本、一份真实数据”的心智位置。

30 秒上手:三种安装方式

DuckDB 是单文件嵌入式引擎,CLI、Python、Node 都能用,零守护进程。下面三条任选其一即可。

# 1) Python
pip install duckdb

# 2) macOS / Linux brew
brew install duckdb

# 3) Node
npm install duckdb

# 进入交互式 CLI
duckdb

直接查 CSV:不用建表

DuckDB 把文件路径当成表来读,read_csv_auto 会自动推断列类型与分隔符。下面这条命令直接对 CSV 做分组汇总,比先 pandas 全量读入再 groupby 省内存得多。

如果遇到非标准分隔符、乱码或类型推断错误,可以给 read_csv_auto 显式传参:delim 指定分隔符、sample_size 控制类型推断的采样行数、all_varchar=true 先整列读成字符串再自行转换,或在 types 里逐列声明。对带 BOM 的 Excel 导出 CSV,加 encoding='utf-8' 通常就能解决。先在小样本上验证类型,再对全量跑聚合,是避免“数字被读成字符串导致排序错乱”的稳妥习惯。

# 直接用 SQL 查 CSV,无需建表、无需建库
duckdb -c "SELECT region, SUM(amount) AS gmv
           FROM 'orders.csv'
           WHERE amount > 0
           GROUP BY region
           ORDER BY gmv DESC;"

Parquet 列式查询与分区剪枝

Parquet 是列式存储,DuckDB 读取时只加载用到的列,并按分区字段做剪枝——只扫符合条件的分区目录,跳过无关文件。对按天分区的日志目录,这一点能省下数量级的时间。

想确认 Parquet 的字段与压缩方式,可执行 DESCRIBE SELECT * FROM 'events.parquet' 看推断出的 schema,再用 COPY (SELECT ...) TO 'out.parquet' (FORMAT PARQUET) 把结果落盘。DuckDB 写的 Parquet 默认用 Snappy 压缩,体积小、可被其它工具(如 Spark、Arrow)直接读取,是理想的中间交换格式。当上游数据来自 关系型数据库 的导出时,用 Parquet 做中转往往比直接塞 CSV 更稳。

# 读取单个 Parquet
duckdb -c "SELECT * FROM 'events.parquet' LIMIT 5;"

# 读取按 dt=YYYY-MM-DD 分区的目录(仅扫 9 月之后的分区)
duckdb -c "SELECT COUNT(*) AS cnt
           FROM 'logs/*/*.parquet'
           WHERE dt >= '2026-09-01';"

用 Python 做数据分析

pandas 的轻量替代

在 Python 里,DuckDB 可以直接消费 DataFrame,也能把查询结果转回 DataFrame。把大聚合下推到 DuckDB 引擎,既快又省内存,是处理中等规模数据的好组合。

除了直接查询 DataFrame,还可以用 duckdb.register('t', df) 把 pandas / Arrow 表注册成具名视图,之后就能用标准 SQL 反复引用;分析完的结果也能用 COPY (...) TO 'result.parquet' 落盘,避免把大结果集整体搬回 Python。这种“SQL 在前、Python 在后”的模式,既保留了 SQL 的表达力,又不影响后续用 Python 画图或接入 检索评测 这类流程。

import duckdb
import pandas as pd

df = pd.DataFrame({"x": range(1000000), "g": range(1000000)})

# 把聚合下推给 DuckDB,只取结果回 Python
q = ("SELECT g % 10 AS bucket, COUNT(*) AS n, SUM(x) AS s "
     "FROM df WHERE x >= 0 GROUP BY 1 ORDER BY 1")
out = duckdb.query(q).df()
print(out)

把远程数据库当外表

DuckDB 支持通过 ATTACH 把 MySQL、PostgreSQL 甚至 SQLite 挂成外部库,于是你可以把“库里的历史订单”和“本地新 CSV”做一次跨源 JOIN,而不用先导出再导入。这对临时对账、补数非常顺手。

挂外部库时建议用只读模式(READ_ONLY)避免误改线上库,敏感密码通过 SECRET 语句或环境变量注入而非硬编码。DuckDB 的 ATTACH 也支持 SQLite、PostgreSQL,思路完全一致,是做跨源临时分析时最省事的“胶水层”。它不要求你在本地再装一套对应驱动重的客户端,一个二进制就够。

-- 把远程 MySQL 挂为外表(注意连接串里的 & 需转义)
ATTACH 'mysql:shop?user=app&password=secret&host=10.0.0.5' AS src (TYPE mysql);

-- 本地 CSV 与远程历史表 JOIN
SELECT o.region, SUM(o.amount) AS gmv
FROM 'orders.csv' o
JOIN src.orders_hist h ON o.id = h.id
GROUP BY 1;

实战:清洗一份混乱的访问日志并出报表

真实日志往往是 gzip 压缩、无表头、字段错位。用 read_csv_auto 显式声明列,再用 strptime 把时间字符串转成时间戳,几行 SQL 就能出 Top 路径报表。这也是 向量检索评测向量数据库 这类数据预处理环节的常见前置动作。

这个模式的价值在于:日志清洗、字段对齐、时间解析这些脏活全交给 SQL 完成,输出已经是规整的聚合表,后续接看板或存成 Parquet 都省事。当数据量继续增长,只需把视图换成对分区目录的通配读取,SQL 几乎不用改,分析口径也始终一致。

-- 读 gzip 日志,显式指定列类型
CREATE VIEW access AS
SELECT *,
       strptime(ts, '%Y-%m-%dT%H:%M:%S') AS t
FROM read_csv_auto('access.log.gz',
                   header=false,
                   columns={'ts':'VARCHAR','ip':'VARCHAR',
                            'path':'VARCHAR','bytes':'BIGINT'});

-- 出报表:Top 10 路径的命中数与人均字节
SELECT path,
       COUNT(*) AS hits,
       AVG(bytes) AS avg_bytes
FROM access
WHERE bytes > 0
GROUP BY path
ORDER BY hits DESC
LIMIT 10;

性能要点与避坑

坑点现象正确做法
全量读 CSV 再过滤内存爆、慢让 DuckDB 先 WHERE 下推,别先 .df()
用 pandas 做大聚合占内存且慢用 duckdb.query 把聚合下推到引擎
忘记分区目录通配漏数据用 ‘dir/*/*.parquet’ 通配命中分区
大结果集直接 .df()OOM先聚合/采样再取,或写回 Parquet
多线程并发写同一库锁冲突单写入或分段落库,读可并发

什么时候用 DuckDB,什么时候上数仓

维度DuckDB数仓 / Spark
部署进程内,零依赖集群、运维成本
数据规模单机十亿行内PB 级
时延亚秒到秒批处理分钟级
适用探索、报表、ETL 前置全员 BI、流式管道

小结

DuckDB 的价值在于“把一个分析型数据库塞进了命令行和一行 import”。它不取代 MySQL 这类事务库,也不硬刚 PB 级数仓,而是补齐了“手头有文件、想用 SQL 立刻算”的那块空白。把 duckdb -c 写进 CI 脚本 做数据校验,或用 Python 下推聚合,你会很快离不开它。当你需要让更多人共享、需要流式与权限治理时,再平滑地把逻辑迁到正式数仓即可——而那段 SQL,几乎可以原样复用。

上一篇 开源许可证选型指南:MIT、Apache、GPL 商用边界
下一篇 LLM 流式输出实战:SSE 推流与前端渲染