DuckDB 指南
你有没有遇到过这种情况:一个 2GB 的 CSV 文件,用 Pandas 读了 40 秒,内存直接飙到 8GB,风扇开始狂转。换成 DuckDB,3 秒搞定,内存几乎没动。
这不是夸张,这是 DuckDB 最直观的卖点——在你自己的电脑上,用 SQL 跑大数据分析,不用装任何数据库服务。
如果要用一句话概括 DuckDB,它就是数据分析界的 SQLite,一个随身携带的分析引擎。SQLite 解决了"我想在本地存点数据但不想装 MySQL"的问题,DuckDB 解决的是"我想在本地分析大数据但不想装 Spark"的问题。

先说结论:DuckDB 适合谁
别浪费时间,先看看你是不是目标用户:
- 数据分析师:天天跟 CSV、Parquet 打交道,Pandas 写到怀疑人生的那种
- 数据工程师:需要在本地快速验证 ETL pipeline,不想每次都连远程数据库
- 后端开发者:想在应用里嵌入轻量分析能力,又不想维护一个独立的数据库服务
- 机器学习工程师:特征工程阶段需要对大量数据做聚合、筛选、join
- 学生和自学者:想学 SQL 但不想折腾 PostgreSQL 安装配置
如果你的日常工作是处理几百 MB 到几十 GB 的数据,而且经常在本地做分析,DuckDB 基本上是目前最好的选择。
DuckDB 的核心能力
1. 直接用 SQL 查询文件——这个功能真的会上瘾
我第一次用 DuckDB 替代 pd.read_csv 的时候,感觉就像从自行车换成了电动车——不是快一点点,是质的飞跃。当时有个 3GB 的用户日志,Pandas 读了快一分钟,DuckDB 两三秒就出结果了。从那以后我就再也回不去了。
不用先把数据导入数据库,直接对文件写 SQL:
-- 查询本地 CSV 文件
SELECT city, COUNT(*) as order_count, AVG(amount) as avg_amount
FROM 'orders_2024.csv'
WHERE status = 'completed'
GROUP BY city
ORDER BY order_count DESC
LIMIT 10;
-- 查询 Parquet 文件(列式存储,速度更快)
SELECT * FROM 'user_events/*.parquet'
WHERE event_date >= '2024-01-01';
-- 甚至可以直接查远程文件
SELECT * FROM 'https://example.com/data/sales.csv'
LIMIT 100;
FROM 后面直接写文件路径,通配符、远程 URL、CSV / Parquet / JSON 都支持。头几次用的时候总觉得"不对吧,这也行?",但它就是行。
2. 和 Python 无缝集成
说实话,我之前对"在 Python 里写 SQL"这件事是有偏见的——总觉得两种语言混着写很别扭。直到用了 DuckDB,才发现它做到了一种很舒服的共存:SQL 负责查询和聚合,Python 负责后续处理和画图,各干各的。
import duckdb
import pandas as pd
# 直接查询 Pandas DataFrame,不需要导入
df = pd.read_csv('users.csv')
result = duckdb.sql("SELECT age, COUNT(*) FROM df WHERE country = 'AU' GROUP BY age")
# 查询结果直接转回 DataFrame
result_df = result.df()
# 也可以转成 Polars DataFrame
result_pl = result.pl()
# 或者直接转成 Python 列表
rows = result.fetchall()
注意这里的关键:DuckDB 可以直接查询 Python 变量名。你定义了一个叫 df 的 DataFrame,SQL 里直接写 FROM df 就能用。不需要注册表、不需要创建连接,开箱即用。第一次见到这个设计的时候我愣了几秒——"等等,它怎么知道 df 是什么?"答案是 DuckDB 会自动扫描当前 Python 作用域里的变量。有点黑魔法,但确实好使。
3. 列式存储:为什么分析场景下它碾压 SQLite
这部分稍微偏原理,不感兴趣可以跳过,但我觉得理解了之后会更有信心在生产环境使用它。
传统数据库(包括 SQLite)按行存储。跑 SELECT AVG(salary) FROM employees 时,它把每一行的所有列都读出来,哪怕你只关心 salary。DuckDB 是列式存储,同样的查询只读 salary 那一列,I/O 差了一个数量级。实测下来分析场景比 SQLite 快 10-100 倍不夸张。
打个比方:行式存储像翻一整本书找章节标题,列式存储像直接看目录。
4. 向量化执行
DuckDB 处理数据不是一行一行来的,而是一批一批(通常 2048 行一批),能更好地利用 CPU 缓存和 SIMD 指令。具体细节不展开了,你只需要知道:这是它在分析查询上跑得快的另一个核心原因。

真实使用场景
场景一:本地大文件分析
公司给你一个 5GB 的用户行为日志 CSV,让你分析过去 30 天的活跃用户趋势。
用 Pandas 的痛苦:pd.read_csv() 先把 5GB 全部加载到内存,你的 16GB 笔记本直接卡死。就算用 chunksize 分块读取,代码写起来又臭又长。
用 DuckDB 的体验:
import duckdb
result = duckdb.sql("""
SELECT
DATE_TRUNC('day', event_time) as day,
COUNT(DISTINCT user_id) as dau
FROM 'user_events_202*.csv'
WHERE event_time >= CURRENT_DATE - INTERVAL 30 DAY
GROUP BY 1
ORDER BY 1
""").df()
print(result)
几行代码,几秒出结果。DuckDB 在底下做了流式读取,不会一股脑把 5GB 全塞进内存。这种"看起来简单但底下做了很多事"的体验,是我喜欢 DuckDB 的主要原因。
场景二:ETL 原型开发
这是我个人最高频的使用场景。写数据管道之前,总得在本地先跑通逻辑吧?以前要么连远程数据库(慢),要么本地装个 PostgreSQL(麻烦)。现在直接 DuckDB 搞定:
import duckdb
con = duckdb.connect('prototype.duckdb')
# 从不同格式的文件创建表
con.sql("CREATE TABLE orders AS SELECT * FROM 'orders.parquet'")
con.sql("CREATE TABLE users AS SELECT * FROM 'users.csv'")
con.sql("CREATE TABLE products AS SELECT * FROM 'products.json'")
# 验证你的 ETL 逻辑
con.sql("""
CREATE TABLE order_summary AS
SELECT
u.country,
p.category,
DATE_TRUNC('month', o.created_at) as month,
SUM(o.amount) as total_revenue,
COUNT(DISTINCT o.user_id) as unique_buyers
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
GROUP BY 1, 2, 3
""")
# 验证没问题后,导出结果
con.sql("COPY order_summary TO 'output.parquet' (FORMAT PARQUET)")
跑通了就迁移到 BigQuery 或 Snowflake,SQL 几乎不用改。省掉的不只是时间,还有心智负担。
场景三:Notebook 里的数据探索
在 Jupyter 里做探索性分析的时候,窗口函数、CTE 这些操作用 Pandas 写总是很别扭,用 SQL 就自然多了:
# 安装 JupySQL 扩展
# pip install jupysql duckdb-engine
%load_ext sql
%sql duckdb://
%%sql
SELECT
department,
ROUND(AVG(salary), 2) as avg_salary,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) as median_salary,
COUNT(*) as headcount
FROM 'employees.csv'
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY avg_salary DESC
你试试用 Pandas 写一个 PERCENTILE_CONT,再对比 SQL 这几行,就知道为什么越来越多人在 Notebook 里混着用 SQL 了。
DuckDB vs Pandas vs SQLite 详细对比
| 对比维度 | DuckDB | Pandas | SQLite |
|---|---|---|---|
| 定位 | 嵌入式 OLAP 数据库 | 数据操作库 | 嵌入式 OLTP 数据库 |
| 查询语言 | SQL(兼容 PostgreSQL 语法) | Python API | SQL(语法较基础) |
| 存储方式 | 列式存储 | 内存中的 DataFrame | 行式存储 |
| 大文件处理 | 出色,流式读取不吃满内存 | 全量加载,内存容易爆 | 需要先导入,分析查询慢 |
| 多表 JOIN | 原生支持,优化器自动选执行计划 | merge() 写法不直观 | 支持,但大表性能差 |
| 窗口函数 | 完整支持 | 有但写法复杂 | 支持 |
| 并发写入 | 单写多读 | 不适用 | 单写多读 |
| 适合数据量 | MB 到几十 GB | MB 到几 GB(看内存) | KB 到几 GB |
| 学习成本 | 会 SQL 就能上手 | 需要学 Pandas API | 会 SQL 就能上手 |
| 生态集成 | Python, R, Node.js, Rust, Java | Python 为主 | 几乎所有语言 |
简单来说:分析聚合选 DuckDB,数据清洗选 Pandas,应用存储选 SQLite。三者不是互斥的——我自己最常见的工作流是 DuckDB 做完聚合,.df() 转成 Pandas 再用 matplotlib 画图。各司其职,配合得很好。
最容易翻车的地方
1. DuckDB 不适合做应用数据库
这点我要特别强调,因为我见过有人踩这个坑。DuckDB 是 OLAP(分析),不是 OLTP(事务)。别拿它替代 SQLite 或 PostgreSQL 存用户数据、处理并发写入——它的写入是单线程的,高并发直接出问题。
2. 内存还是有上限的
DuckDB 内存管理确实比 Pandas 好一大截,但不是无限的。100GB+ 的数据集还是会遇到瓶颈,这时候老老实实上 Spark 或 BigQuery。
3. 持久化数据库文件的坑——我自己也中过招
# 内存模式(默认)——关掉就没了
con = duckdb.connect()
# 持久化模式——数据存在文件里
con = duckdb.connect('my_data.duckdb')
我有一次花了大半个下午清洗数据、建好了几张中间表,结果忘了加文件路径,用的内存模式。关掉 Jupyter 的一瞬间人就傻了。从此之后养成习惯:只要不是写一次性脚本,第一行永远是 con = duckdb.connect('xxx.duckdb')。
4. 版本兼容性
DuckDB 迭代很快,不同版本的持久化文件格式可能不兼容。升级版本后旧的 .duckdb 文件有可能打不开。所以重要数据务必同时导出一份 Parquet 备份,这个习惯能救命。
5. 跨进程并发写入
多个 Python 进程同时写同一个 DuckDB 文件会直接报错。需要多进程写入的话,要么换 PostgreSQL,要么每个进程写自己的文件最后合并。这个限制不算 bug,是 DuckDB 作为嵌入式数据库的设计取舍。
安装与快速上手
安装
# Python(最常用)
pip install duckdb
# Node.js
npm install duckdb
# 命令行工具(macOS)
brew install duckdb
# 命令行工具(Linux)
# 从 GitHub Releases 下载:https://github.com/duckdb/duckdb/releases
30 秒快速体验
import duckdb
# 1. 直接查询 CSV
print(duckdb.sql("SELECT * FROM 'data.csv' LIMIT 5"))
# 2. 做聚合分析
print(duckdb.sql("""
SELECT category, SUM(revenue) as total
FROM 'sales.csv'
GROUP BY category
ORDER BY total DESC
"""))
# 3. 和 Pandas 配合
import pandas as pd
df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 'score': [85, 92, 78]})
result = duckdb.sql("SELECT * FROM df WHERE score > 80").df()
print(result)
# 4. 导出结果到 Parquet
duckdb.sql("COPY (SELECT * FROM 'raw_data.csv' WHERE amount > 100) TO 'filtered.parquet' (FORMAT PARQUET)")
命令行使用
# 启动交互式 SQL 终端
duckdb
# 直接对文件执行查询
duckdb -c "SELECT COUNT(*) FROM 'logs.csv'"
# 打开已有的数据库文件
duckdb my_data.duckdb
进阶技巧
用 DuckDB 替代 pd.read_csv()
当 CSV 文件较大时,用 DuckDB 读取比 Pandas 快得多:
import duckdb
# 比 pd.read_csv() 快 5-10 倍
df = duckdb.sql("SELECT * FROM 'large_file.csv'").df()
查询远程 S3 文件
import duckdb
con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")
con.sql("SET s3_region = 'ap-southeast-2';")
result = con.sql("SELECT * FROM 's3://my-bucket/data/*.parquet' LIMIT 100")
多文件批量查询
-- 查询目录下所有 CSV
SELECT * FROM 'data/2024/*.csv';
-- 带文件名列,知道每行来自哪个文件
SELECT filename, * FROM glob('data/2024/*.csv');
相关资源
- Pandas 指南 — 数据清洗和逐行操作的首选工具
- Polars 指南 — 另一个高性能 DataFrame 库,和 DuckDB 互补
- Jupyter 指南 — 在 Notebook 中使用 DuckDB 的最佳搭档
- DuckDB 官网:https://duckdb.org
- DuckDB 文档:https://duckdb.org/docs/
- DuckDB GitHub:https://github.com/duckdb/duckdb
