DuckDB 深度解析:把分析型数据库塞进进程内——从列式存储、向量化执行引擎到零拷贝与分布式 Smallpond 的完整实战(2026)
关键词:进程内 OLAP、列式存储、向量化执行、Apache Arrow 零拷贝、ASOF JOIN、Smallpond、本地优先分析
适用读者:写过
SELECT * FROM pandas_df却卡在性能上的数据工程师、想在不堆集群的前提下做轻量分析的开发者、以及所有受够了"为了一次性的分析去部署一套 PostgreSQL / ClickHouse"的人。
〇、写在前面:为什么你需要认真看一个"嵌入式数据库"
如果你是一名后端或数据工程师,大概率经历过这样的场景:
- 一份 3 GB 的 CSV 落在对象存储里,老板让你"算一下上个月各地区的 GMV 同比"。你不想为了这个一次性需求去申请一套 ClickHouse,也不想用 Pandas 把内存撑爆——但你确实只想写一句 SQL。
- 你在 Python 里清洗数据,96% 的时间花在
df.groupby(...).agg(...)上,而这些操作 Pandas 跑得又慢又吃内存。你隐约觉得:"这种事明明数据库最擅长,为什么我非要用 DataFrame?" - 你在做特征工程 / 离线报表,数据在 Parquet 里,你在 Pandas、Polars、Spark 之间反复横跳,拷贝来拷贝去,内存翻倍。
这些痛点的共同根源是:数据分析的"中间地带"长期没有合适的工具——数据量远超 Excel / SQLite 的上限(百万级到十亿级),但又远没到需要分布式集群的规模。为了这个中间地带去运维一个 Client-Server 数据库,成本得不偿失。
DuckDB 就是为这个中间地带而生的。它的口号是 "SQLite for Analytics":像 SQLite 一样嵌入式、零依赖、单文件,但底层是列式存储 + 向量化执行引擎,专门吃分析型(OLAP)查询。这篇文章不会停留在"DuckDB 真香"的层面,而是带你把它的存储模型、执行引擎、与 Arrow 的零拷贝通道、以及生产级用法彻底拆开,并给出可直接抄的代码。
一、背景:被忽视的"中间地带"与 DuckDB 的来路
1.1 数据的"尺寸光谱"与工具错配
把数据处理工具按"典型数据量"排成一条光谱,会发现一个尴尬的空洞:
| 工具 | 执行引擎 | 部署形态 | 典型数据量 | 强项 |
|---|---|---|---|---|
| SQLite | 行式 OLTP | 嵌入式 | < 100 万行 | 事务、CRUD、单机应用 |
| Pandas | Python 内存库 | 进程内 | < 500 万行(内存允许) | 灵活的数据变形 |
| DuckDB | 列式 OLAP | 嵌入式 | 100 万 ~ 100 亿行 | SQL 分析、零配置 |
| PostgreSQL | 行式 OLTP | 服务器 | 1000 万 ~ 100 亿行 | OLTP + 轻 OLAP |
| ClickHouse | 列式 OLAP | 服务器 / 集群 | 100 亿+ 行 | 极致分析吞吐 |
| Spark | 分布式 | 集群 | TB ~ PB | 超大规模批处理 |
注意 DuckDB 那一档:100 万到 100 亿行。这恰恰是绝大多数"真实业务分析"所在的位置。可传统选择只有两个极端:要么把数据塞进内存用 Pandas 硬算(慢、易 OOM),要么上服务器级数据库(运维成本高)。DuckDB 把"列式分析引擎"直接搬进了你的进程,把这一段光谱填平了。
一个常被忽略的事实:DuckDB 能直接流式读取比内存大得多的文件。它不需要把全表 load 进内存,而是按列、按块(block)读取并流水线处理。所以"内存只有 16 GB,但 CSV 有 30 GB"这种事,DuckDB 照样能
SELECT SUM(amount) FROM 'big.csv'。
1.2 DuckDB 的来路:不是玩具,是学术工程的产物
DuckDB 并非某个公司的 KPI 产物,而是源于荷兰 CWI(Centrum Wiskunde & Informatica,关系型数据库理论的发源地之一,SQL 标准、MonetDB 都出自这里)的研究成果。核心作者是 Mark Raasveldt 和 Hannes Mühleisen,2019 年发布首个版本。它的学术血统意味着:向量化执行、列式存储这些设计不是"抄个热点",而是继承自 MonetDB / Vectorwise 一脉的成熟理论。
它的设计哲学可以浓缩成四句话:
- 嵌入式(Embedded):没有服务器进程,没有 socket,库直接在宿主进程里跑。
- 零依赖(Zero Dependencies):核心是一个几 MB 的单一二进制,Python 包
pip install duckdb装完即用。 - 面向分析(OLAP-first):牺牲点查(point lookup)性能,把聚合、扫描、连接做到极致。
- 与生态系统共生:原生支持 Apache Arrow、Pandas、Parquet、CSV、JSON,而非另起炉灶。
二、核心概念:三个你必须真正理解的根本决策
很多人用 DuckDB 只停留在 import duckdb; duckdb.query(...),但真正决定它"为什么快"的是三个底层决策。不理解这三个,你就只会把它当成一个"能跑 SQL 的 Pandas",用不出精髓。
2.1 决策一:进程内嵌入式(In-Process / Serverless)
传统数据库(PostgreSQL、MySQL、ClickHouse)是 Client-Server 架构:你的应用通过网络协议把 SQL 发给另一个进程,对方解析、执行、把结果序列化后通过网络传回来。每一次查询都有:
- 网络往返延迟(即使本地 localhost 也有 Unix socket / TCP 开销)
- 序列化 / 反序列化成本(行先变成 wire format,再变回内存对象)
- 进程间内存拷贝(数据从数据库进程的用户态拷到你的进程)
DuckDB 直接在你的 Python / R / Node 进程地址空间里执行查询。没有网络,没有 IPC,查询结果就是一块你进程里的内存。这意味着:
import duckdb
import pandas as pd
# DuckDB 直接吃进 Pandas DataFrame,甚至可以零拷贝(见第四章)
con = duckdb.connect()
df = pd.DataFrame({"x": range(1_000_000), "y": range(1_000_000)})
con.register("t", df) # 把 DataFrame 注册成一张表
print(con.execute("SELECT SUM(x*y) FROM t").fetchone())
代价是什么?因为是单进程,DuckDB 是 单写者(single-writer) 模型——它不擅长高并发的点写(那是 SQLite / Postgres 的活)。但反过来,它把"读分析"做到了极致。这 perfectly 契合它的定位:分析型负载,写少读多。
2.2 决策二:列式存储(Columnar Storage)
行式存储(SQLite、Postgres)把一行的所有字段连续存放:[id, name, age, salary] [id, name, age, salary] ...。这种布局对"取某一行"极快,但对"算所有员工的平均工资"是灾难——为了读 salary 一列,CPU 得把整行(包括无关的 name、id)都捞进缓存。
列式存储反过来:把同一列的值连续存放:[salary, salary, salary, ...] [age, age, age, ...]。于是:
- 聚合只需读相关列:
SELECT AVG(salary)只读 salary 这一块,I/O 和缓存利用直接降一个数量级。 - 列内压缩率极高:同一列的数据类型相同、取值范围相近(比如年龄都在 18~65),可以用字典编码(Dictionary Encoding)、游程编码(RLE)、比特压缩(Bitpacking)压得很小,进一步减少 I/O。
- 向量化友好:连续同类型的值天然适合 SIMD 批量处理。
DuckDB 在磁盘(或文件)上的物理布局是:数据被切成 Row Group(行组),每个 Row Group 内每个列独立存成一个 Column Chunk,Column Chunk 再切成固定大小的 Block。每个 Row Group 还附带 min/max/null_count 等统计元数据——这一点在第五章的"统计剪枝"里是性能关键。
-- 列存让"只读需要的列"成为默认行为
-- 下面这条查询,DuckDB 物理上根本不会去读 name / address 列
SELECT region, SUM(amount)
FROM sales
WHERE dt BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY region;
2.3 决策三:向量化执行(Vectorized Execution)
这是 DuckDB 与"朴素 SQL 引擎"最大的分水岭。
传统执行引擎(所谓 Volcano / Iterator 模型)是**逐行(tuple-at-a-time)**的:执行树的每个算子实现一个 next() 接口,父算子每调一次 next(),子算子就吐出一行,这一行穿过整棵算子树做一次计算。问题在于:
- 每行都要做一次虚函数调用(virtual dispatch),上亿行就是上亿次分支预测失败。
- 一次只处理一个值,CPU 的 SIMD 向量指令、预取器、缓存行全部浪费。
- 解释执行式的逐行循环,现代 CPU 的深流水线跑不起来。
DuckDB 用的是 向量化(Vectorized / Batch)模型:每个算子一次处理一批(默认 2048 行)的数据,称为一个 Vector。这一批数据以列式 Vector 的形式在算子间流动。好处立竿见影:
- 一次虚调用处理 2048 行,虚调用次数直接除以 2048。
- Vector 内是同类型连续值,可以走 SIMD(比如一次比较 8 个 int32)、分支预测几乎全中(同一批走同一代码路径)。
- 数据以 cache line 对齐的列式块流动,缓存命中率拉满。
用一个最小例子感受区别:同样是 filter(x > 100),逐行版本是 for row in rows: if row.x > 100: out.append(row);向量化版本是一次性对 2048 个 x 做 x > 100 的向量比较,得到一个布尔掩码,再按掩码挑出存活行。后者在编译器和 CPU 层面都能被优化到飞起。
深度提示:DuckDB 的向量化是**拉取式(pull-based)**的——上层算子向下层要一个 Vector,下层算子返回一个填好的 Vector。同时它把执行计划切成多个 Pipeline(流水线),每个 Pipeline 内的算子链式处理同一批 Vector,减少中间物化(materialization)。这套设计直接继承自 Vectorwise / MonetDB/X100 的研究。
三、架构分析:一条 SELECT 是怎么跑完的
理解架构,才能理解"我写的 SQL 到底被怎么对待了"。DuckDB 的一条查询经历以下阶段:
SQL 文本
→ Parser(词法/语法分析,生成 AST)
→ Binder(语义绑定:表/列是否存在、类型推导、作用域解析)
→ Logical Optimizer(基于代价的优化 + 启发式规则重写)
→ Physical Planner(把逻辑计划变成可执行的物理算子)
→ Execution Engine(向量化执行,按 Pipeline 跑)
→ 结果(Arrow / Pandas / Python 对象)
3.1 优化器:它比你想象中聪明
DuckDB 的优化器不止是"重排 JOIN 顺序"那么简单,它做了大量对分析负载极有价值的重写:
- 谓词下推(Predicate Pushdown):把
WHERE条件下推到扫描算子里,扫描文件时就只捞符合条件的行。 - 投影下推(Projection Pushdown):只读 SQL 真正用到的列(配合列存,I/O 直接降维)。
- 统计剪枝(Statistical Pruning):利用 Row Group 的 min/max 元数据,整段跳过不可能包含目标数据的 Row Group。对 Parquet 这种带统计块的文件尤其致命(快)。
- 连接顺序重排、子查询去关联、CTE 复用、常量折叠等。
-- 即使你写了子查询和复杂表达式,优化器也会尽量下推
SELECT region, cnt FROM (
SELECT region, COUNT(*) AS cnt
FROM 's3://bucket/sales_*.parquet' -- 上千个分区文件
WHERE dt >= '2026-01-01'
GROUP BY region
) WHERE cnt > 1000;
-- 这里的 dt 过滤会被下推到 Parquet 扫描层,
-- 且每个文件的 Row Group 若 min(dt) > 边界会被整段跳过
3.2 执行引擎与并行:Morsel-Driven Parallelism
DuckDB 不是单线程跑向量的。它内置了一个任务调度器,采用 Morsel-Driven Parallelism(基于"小片"的并行):把数据切成小块(morsel),多个线程各自认领 morsel 并行执行同一个 Pipeline,最后再合并(比如把各线程的局部聚合 merge 成全局聚合)。
这意味着你 import duckdb 后什么都不用配,它默认就把你机器的多核用起来了。可以通过 SET threads=4; 控制并行度(调试或资源隔离时有用)。
3.3 与 Apache Arrow 的零拷贝通道(这是灵魂)
很多"SQL 引擎 + Python"的集成,结果要从 C++ 世界序列化到 Python 对象,再变成 Pandas,拷贝两三遍。DuckDB 不一样:它的内存中数据表示,就是 Apache Arrow 格式。
Arrow 是一套跨语言的内存列式数据标准。DuckDB 的结果本身就是一块合法的 Arrow buffer。所以:
- 查询结果是 Arrow Table 时,转成 Pandas / Polars 几乎不拷贝内存(只是换了个视图/外壳)。
- 反过来,你把一个 Arrow / Pandas(基于 Arrow 的 Pandas 2.x)对象注册给 DuckDB,DuckDB 也能直接零拷贝地把它当表用。
import duckdb, pandas as pd
con = duckdb.connect()
df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
# zero-copy:DuckDB 直接引用 df 底层的 Arrow buffer,不复制数据
con.register("df", df)
res = con.execute("SELECT a, b FROM df WHERE a > 1").arrow() # 返回 Arrow Table
back = res.to_pandas() # Pandas 也基于 Arrow,仍是近乎零拷贝
这一点是 DuckDB 在 Python 数据科学生态里"无缝"的根本原因——它不是"又一个数据库",而是嵌进你现有内存数据之上的 SQL 执行层。
3.4 事务与持久化
DuckDB 支持 ACID 事务,采用类似 MVCC 的机制,写操作记录 WAL(Write-Ahead Log)。但它定位是 OLAP,默认 单写者:同一时刻只有一个写事务。读取完全无锁、可并发。持久化可以是单文件(.duckdb),也可以纯内存(:memory:)。
四、代码实战:从随手查到生产级 ETL
理论讲完,上真代码。下面所有片段都是可运行的(Python 3.10+,DuckDB 1.x)。
4.1 五分钟上手:直接对文件写 SQL,不用建表
DuckDB 最爽的特性之一:不用 ETL 进数据库,直接对原始文件查询。
import duckdb
# 1) 直接查 CSV(自动推断 schema、类型)
rows = duckdb.query("""
SELECT region, SUM(amount) AS gmv
FROM 'data/sales_2026.csv'
WHERE dt >= '2026-06-01'
GROUP BY region
ORDER BY gmv DESC
""").fetchall()
print(rows)
# 2) 直接查 Parquet,且支持通配符跨多文件
duckdb.query("""
COPY (
SELECT * FROM 'data/sales_2026-*.parquet'
WHERE region = '华东'
) TO 'out/east_china.parquet' (FORMAT PARQUET)
""")
# 3) 直接查远程 HTTP(S) 上的文件(无需先下载!)
# 常用于数据湖 / 对象存储上的 Parquet
duckdb.query("""
SELECT COUNT(*) FROM read_parquet('https://example.com/warehouse/orders/*.parquet')
""")
注意 read_parquet / read_csv 是表函数(table-producing function),可以像表一样放进 FROM。这比"先 download 再 load"省了巨量时间和磁盘。
4.2 与 Pandas / Arrow 的零拷贝互操作
import duckdb
import pandas as pd
import pyarrow as pa
con = duckdb.connect()
# 准备一个 Pandas DataFrame(Pandas 2.x 底层就是 Arrow)
df = pd.DataFrame({
"user_id": [1, 2, 3, 1, 2],
"event": ["click", "view", "click", "purchase", "view"],
"ts": pd.to_datetime(["2026-06-01", "2026-06-01", "2026-06-02",
"2026-06-02", "2026-06-03"]),
})
# 注册为零拷贝视图
con.register("events", df)
# 用 SQL 做漏斗/留存分析,比 Pandas groupby 简单且更快
sql = """
SELECT user_id,
COUNT(*) FILTER (WHERE event = 'click') AS clicks,
COUNT(*) FILTER (WHERE event = 'purchase') AS buys
FROM events
GROUP BY user_id
"""
result_arrow = con.execute(sql).arrow() # 拿 Arrow Table(零拷贝)
result_df = con.execute(sql).df() # 或直接拿 Pandas
print(result_arrow)
FILTER (WHERE ...) 是 SQL 标准语法,在 DuckDB 里做"同一份数据上算多种指标"比 Pandas 的多次 groupby 优雅太多,而且只扫描一次。
4.3 进阶 SQL:几个让数据分析师尖叫的特性
DuckDB 实现了相当完整的现代 SQL,下面三个是"用过就回不去"的。
(1)ASOF JOIN —— 时序"最近匹配"
做金融、物联网、埋点分析时常见需求:"给每笔交易匹配它之前最近的一笔报价"。普通 JOIN 做不到"不等值最近",ASOF JOIN 一行解决:
-- trades 每笔交易,quotes 持续报价;给每笔交易找成交前最近的报价
SELECT t.symbol, t.ts AS trade_ts, q.ts AS quote_ts, q.price AS last_quote
FROM trades t
ASOF JOIN quotes q
ON t.symbol = q.symbol
AND t.ts >= q.ts -- 匹配"时间上不晚于交易"的最近一条
ORDER BY t.ts;
(2)PIVOT / UNPIVOT —— 行列互转
-- 把"长表"透视成"宽表":每个 region 一列
PIVOT sales
ON region
USING SUM(amount)
GROUP BY dt
ORDER BY dt;
(3)QUALIFY —— 给窗口函数结果加过滤
窗口函数不能直接出现在 WHERE 里(执行顺序问题),以前要套一层子查询。DuckDB 支持 QUALIFY,像给窗口函数加 WHERE:
-- 取每个用户消费额 Top 3 的订单
SELECT user_id, order_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn
FROM orders
QUALIFY rn <= 3;
(4)LIST / STRUCT 嵌套类型 + JSON:DuckDB 原生支持 LIST、STRUCT、MAP 和 JSON 类型,json_extract、数组展开 UNNEST 一应俱全,处理半结构化日志非常顺手。
4.4 懒执行的 Relation API 与可组合 ETL
除了写 SQL 字符串,DuckDB 提供 Relation API:用链式方法构造查询,且是**惰性(lazy)**的——在你调用 .execute() / .fetch* 之前,它只在做计划,不会真正扫描数据。这让你能用代码动态拼装流水线:
import duckdb
con = duckdb.connect()
# 从文件创建 Relation(惰性,此时没读数据)
rel = con.read_csv("data/sales_2026.csv")
# 链式变换,全部惰性
pipeline = (
rel.filter("dt >= '2026-06-01'")
.filter("amount > 0")
.aggregate("region, SUM(amount) AS gmv", "region")
.order("gmv DESC")
)
print(pipeline) # 打印的是执行计划,不是数据
df = pipeline.execute().df() # 这里才真正跑
Relation API 适合把"读 → 清洗 → 聚合 → 写出"封装成可复用的 Python 函数,比拼字符串 SQL 更不容易出错。
4.5 高速写入与分区导出
Appender(逐行高速插入):如果你要在程序里逐条产生数据并写入 DuckDB,别用 INSERT 循环(每条都有事务开销),用 Appender:
import duckdb
con = duckdb.connect("metrics.duckdb")
con.execute("CREATE TABLE metrics (ts TIMESTAMP, host VARCHAR, cpu DOUBLE)")
app = con.append("metrics") # 拿到 Appender
for ts, host, cpu in stream(): # 假设实时产生
app.append(ts, host, cpu)
app.close() # 一次性刷盘
分区写出(PARTITION_BY):做数据湖分层时,按日期/地区把结果拆成多个 Parquet 文件,一行搞定:
COPY (
SELECT * FROM sales
) TO 'warehouse/sales/' (
FORMAT PARQUET,
PARTITION_BY (dt, region), -- 按 dt/region 拆目录
COMPRESSION ZSTD
);
五、性能优化:把 DuckDB 跑满的七个要点
很多人觉得"DuckDB 不就是个库吗,装上就行",结果一跑比 Pandas 还慢。问题几乎都在用法。下面按性价比排序。
5.1 第一原则:让数据待在 Parquet 里,别回退到 CSV
CSV 是文本、无类型、无统计、无压缩。DuckDB 读 CSV 要逐字符解析、做类型推断,慢且占内存。把 CSV 先 COPY ... TO parquet 一次,后续查询速度快数倍:
-- 一次性:CSV -> Parquet(带压缩、带统计、列式)
COPY (SELECT * FROM 'raw/big.csv') TO 'clean/big.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
-- 之后所有分析都查 Parquet:谓词下推 + 统计剪枝 + 列裁剪全开
SELECT region, SUM(amount) FROM 'clean/big.parquet' WHERE dt >= '2026-06-01' GROUP BY region;
Parquet 自带每列的 min/max/null 统计,DuckDB 扫描时能整段跳过无关 Row Group——这是 CSV 永远给不了的能力。
5.2 第二原则:用 SQL 做聚合,别拉回 Python 再算
反模式:
# ❌ 把 1 亿行全拉进 Python,内存爆炸,还慢
df = con.execute("SELECT * FROM sales").df()
print(df.groupby("region")["amount"].sum())
正解:
# ✅ 让 DuckDB 在 C++ 引擎里算完,只回传聚合结果
print(con.execute("""
SELECT region, SUM(amount) AS gmv
FROM 'clean/big.parquet'
GROUP BY region
""").fetchall())
DuckDB 的向量化聚合比 Python 循环快一到两个数量级,且只把小结果传回。记住一条铁律:尽量让数据留在数据库引擎里,只把答案搬出来。
5.3 第三原则:利用列裁剪 + 谓词下推,别 SELECT *
SELECT * 会强制读所有列。分析查询只取需要的列,I/O 直接按列数等比下降。配合 Parquet,效果叠加。
5.4 第四原则:控制并行度与内存
SET threads = 8; -- 默认用满核;容器里按需调小
SET memory_limit = '4GB'; -- 限制内存,避免吃光宿主机
SET enable_progress_bar = true; -- 长查询看进度(CLI / 交互环境)
DuckDB 在内存不足时会把中间结果落盘(spill),所以即使数据比内存大也能跑完,只是慢一些。设好 memory_limit 能让它在"优雅落盘"和"硬 OOM"之间选前者。
5.5 第五原则:避免 Python 循环里反复 execute
每次 con.execute 都有解析、计划的开销。批量 / 预处理语句(prepared statements)或用 Relation API 一次性表达逻辑,远比 for 循环里调 SQL 高效。
5.6 第六原则:理解"流式聚合"与"物化"的边界
对于超大数据集的 GROUP BY,DuckDB 默认是哈希聚合(需要把分组键放内存)。如果分组基数极高(比如按 user_id 分组且用户上亿),内存会吃紧。此时可考虑:
- 先按维度预聚合再二次聚合;
- 或利用分区写出,分桶计算;
- 或评估是否真的该上 ClickHouse(见第六章)。
5.7 第七原则:该用索引吗?—— DuckDB 的取舍
DuckDB 没有传统 B-Tree 二级索引(有主键/唯一约束,但主要用于完整性)。它的加速靠的是列存 + 扫描下推 + 统计剪枝 + 并行扫描,而不是"建索引加速点查"。这意味着:
- 全表扫描类分析(OLAP)它极快;
- 但
WHERE id = 12345这种点查,它要扫(或至少扫统计块),比有索引的 Postgres 慢。
所以别拿 DuckDB 当高并发 OLTP 用——那是它主动放弃的战场。
5.8 一个诚实的基准认知
网上常看到"DuckDB 比 Pandas 快 100 倍"的说法。这取决于场景:在"对单表做聚合 / 过滤 / 连接"这类 DuckDB 擅长的负载上,常常快 10~100 倍,且内存占用低一个数量级;但在"大量细碎的逐行 Python 逻辑"上,二者半斤八两(因为逻辑本来就在 Python 里)。正确心态:把分析型操作交给 DuckDB 的 SQL 引擎,把 Pandas 留给它真正擅长的灵活变形。 二者不是替代,是分工。
六、总结与展望:DuckDB 只是起点
6.1 Smallpond / DeepSeek:当 DuckDB 遇上分布式
2025 年底 DeepSeek 开源的 Smallpond,给 DuckDB 指了一条有意思的路:它把 DuckDB 和自研的 3FS(Fire-Flyer File System,高性能分布式文件系统) 结合,做"轻量级数据处理框架"。思路很巧妙——不重造分布式引擎,而是让大量独立的 DuckDB 进程在数据附近各自处理自己那一块(数据局部性 + 分片),再小心地划分工作负载、汇总结果。本质上是用 DuckDB 当"本地计算单元",用分布式文件系统当"共享存储",绕开了 Spark 那种重量级调度。
这印证了一个趋势:单机向量化引擎已经快到,很多"以前必须上分布式"的任务,现在靠"多个 DuckDB + 一个好文件系统"就能解决。 这就是"分布式 DuckDB 的梦想"正在变真实的信号。
6.2 MotherDuck 与"本地优先分析"
MotherDuck 是 DuckDB 背后的商业化公司做的云端版本,把本地 DuckDB 和云上实例打通,支持本地优先(local-first)、按需上云的混合分析。配合 DuckDB-Wasm(浏览器里跑),"在任何地方写同一句 SQL"正在成为现实。
6.3 何时不该用 DuckDB(实用主义底线)
工具没有银弹。以下场景请果断换方案:
- 高并发、多写者、点查为主的 OLTP:上 PostgreSQL / MySQL。DuckDB 是单写者。
- 数据量稳定超过数百亿行且需实时高并发服务:上 ClickHouse / Doris / Databricks。
- 需要多节点强一致事务 / 复杂在线事务:DuckDB 不是为此设计。
- 团队已经重度依赖 Spark 生态且数据真在 PB 级:DuckDB 更适合它的"前处理 / 探索 / 中小规模"环节。
6.4 给你的一条落地建议
把 DuckDB 当成你工具箱里的**"分析瑞士军刀"**:
- 任何"对文件/对象存储里的数据跑一句 SQL"的需求,先想 DuckDB;
- 任何"Pandas groupby 跑得慢、内存炸"的场景,把聚合下推给 DuckDB;
- 任何"数据湖探索 / 特征工程 / 离线报表"的流水线,用 Relation API + Parquet 搭;
- 真遇到它扛不住的规模,再谈集群——那时你已经用它省下了 90% 的不必要架构成本。
DuckDB 的真正价值,不是"又一个数据库",而是它把列式分析引擎的门槛降到了 pip install 级别。当一个分析型数据库可以像 import json 一样随手 import,数据分析的工作方式就变了:你不再为了"算个数"去申请资源、部署服务、搬运数据——你只是写了句 SQL,答案就回来了。
这,才是"把分析型数据库塞进进程内"这件事,最值得兴奋的地方。
附录:快速上手清单
pip install duckdb pyarrow pandas
import duckdb
# 1. 直接查文件
duckdb.query("SELECT region, SUM(amount) FROM 'sales.parquet' GROUP BY 1").show()
# 2. 接 Pandas(零拷贝)
con = duckdb.connect(); con.register("df", my_df)
# 3. 接 Arrow / 远程 Parquet
duckdb.query("SELECT * FROM read_parquet('https://.../x.parquet') LIMIT 10").arrow()
版本说明:本文基于 DuckDB 1.x 系列(2026 年主流版本),SQL 语法与 API 以官方文档为准。示例中的性能表述为典型场景趋势,实际数字因硬件、数据特征而异,请以你自己的基准测试为准。