DuckDB 深度实战:嵌入式 OLAP 如何重塑数据工程——从向量化执行引擎到 Parquet 湖仓查询的完整指南(2026)
如果 data 工程师的工具箱里只能再留一个"瑞士军刀",我会毫不犹豫地把票投给 DuckDB。它不是又一个"更快的数据库",而是把"分析型查询"这件事重新拉回了单机、进程内、零部署的朴素形态。本文从内核原理讲到生产落地,配完整可运行代码,帮你真正把 DuckDB 用起来。
0. 背景:我们为什么需要 DuckDB
先说一个扎心的日常。作为一个后端 / 数据工程师,你大概率经历过下面任意一种场景:
- Spark 太重:就想对 20GB 的日志做个球,结果要先申请集群、等 YARN 调度、写一堆
withColumn和groupBy,跑完一杯咖啡都凉了。你只是想要一个答案,却被迫运营一整套分布式系统。 - Pandas 太脆:
df.groupby(...).agg(...)写起来很爽,但数据一上 5GB,内存直接 OOM,SettingWithCopyWarning还没搞明白,进程先崩了。Pandas 是行式、解释器逐行执行的,它从设计上就不是为"大规模分析"准备的。 - SQLite 偏科:SQLite 是"事务型 SQLite",行式存储,擅长高并发小事务(OLTP),但一旦你丢给它一个
SUM/AVG/GROUP BY跑几千万行的分析查询,它慢得让你怀疑人生——因为它要把整行整行读进内存,哪怕你只关心两列。 - ClickHouse 要养:ClickHouse 很强,但它是客户端-服务器架构,你得先起服务、建表、导数据、管账号。对"我本地就想分析个文件"这种需求,杀鸡用牛刀。
DuckDB 的出现,本质上是回答一个问题:能不能有一个像 SQLite 一样"打开就用、无需服务器",但像 ClickHouse 一样"分析查询飞快"的引擎?
它的官方定位很精准——"Analytics in a box"(开箱即用的分析引擎),或者说"分析领域的 SQLite"。区别在于:
- SQLite:进程内、行式、为 OLTP(事务)优化;
- DuckDB:进程内、列式、为 OLAP(分析)优化。
一句话定义:DuckDB 是一个嵌入式、进程内(in-process)、列式、向量化执行的 OLAP 数据库,专为本地与嵌入式分析场景设计,无需任何服务进程,单文件或纯内存即可运行。
下面我们一步步拆开看它到底强在哪、怎么用、以及什么时候不该用。
一、核心概念:先把几个词嚼碎
在写代码之前,先把支撑 DuckDB 的四个核心概念讲清楚。否则你只会"调 API",遇到性能问题还是两眼一抹黑。
1.1 OLTP vs OLAP
这是数据库世界最基础的一刀切:
| 维度 | OLTP(联机事务) | OLAP(联机分析) |
|---|---|---|
| 典型操作 | 增删改查单行 | 大批量扫描 + 聚合 |
| 数据形态 | 当前状态、窄行 | 历史、宽表、读多写少 |
| 优化目标 | 低延迟、高并发、一致性 | 高吞吐、少 IO、压缩率 |
| 代表 | MySQL、PostgreSQL、SQLite | ClickHouse、Snowflake、DuckDB |
DuckDB 是纯正的 OLAP 引擎。它不追求高并发写入,追求的是"一次扫完几千万行、算一堆聚合,越快越好"。
1.2 行式存储 vs 列式存储
这是 DuckDB 快的根本原因之一。
- 行式(SQLite、MySQL):数据按"行"连续存放。
[id=1, name='a', age=20][id=2, name='b', age=21]...。当你只想要AVG(age)时,引擎被迫把每一整行都读进内存,再把age字段抠出来——大量 IO 被浪费在id、name这些你根本不要的列上。 - 列式:数据按"列"连续存放。
age: [20,21,22...]、name: ['a','b'...]。做AVG(age)时,只需要把age这一列读进内存,IO 量直接砍到需要的列。而且同一列数据类型一致,压缩率极高(字典编码、RLE、位打包)。
DuckDB 的存储与查询执行都是列式的。这意味着:你 SELECT age FROM huge_table 时,它只读取 age 对应的列块,其余列根本不碰——这叫列裁剪(column pruning)。
1.3 向量化执行(Vectorized Execution)
这是 DuckDB 与"传统逐行执行"最本质的区别,也是它比 Pandas 快一个数量级的核心。
传统解释器(如早期 PostgreSQL、Pandas 的逐行循环)是一次处理一行:取出一行 → 计算 → 取出下一行。这种"行级"调用在解释型语言里意味着海量函数调用开销,CPU 的流水线、SIMD 指令、缓存局部性全部被浪费。
DuckDB 采用向量化执行:一次处理一批(默认 2048 行)数据,以"列向量(Vector)"为单位在算子之间传递。好处是:
- 批量调用、摊薄开销:一次函数调用处理 2048 行,函数调用次数降到 1/2048;
- CPU 缓存友好:同一列的数据连续排列,缓存命中率高;
- SIMD 加速:对整批数据做
col + 1、col > 100这类操作,编译器能生成 SIMD 指令,一条指令并行处理多个值; - 惰性物化:很多中间结果停留在"列向量"形态,不必反复序列化成行对象。
你可以把向量化理解成:逐行执行是"流水线工人一个一个装零件",向量化是"整箱零件一起过机器"。
1.4 进程内(In-Process)架构
DuckDB 没有"数据库服务器"这个概念。它就是一个嵌入在你程序进程里的 C++ 库(Python/Node/Go/Rust/Java 等都是对它的薄封装)。
- 没有网络协议栈、没有连接池、没有序列化反序列化开销;
- 数据直接在进程内存里被引擎处理;
- 和 Python 的 DataFrame / Arrow 之间可以零拷贝传递。
对比 ClickHouse:你写 SQL → 客户端序列化 → 网络 → 服务端解析 → 执行 → 序列化结果 → 网络 → 客户端反序列化。DuckDB 省掉了中间所有网络与序列化环节。
二、架构分析:一条 SQL 是怎么跑完的
理解执行链路,才能在出问题时知道该往哪优化。DuckDB 的一条查询大致经历下面几个阶段:
SQL 文本
│
▼
[1] Parser(解析器) → 词法/语法分析,生成 AST(抽象语法树)
│
▼
[2] Binder(绑定器) → 名字解析、类型推导、权限/表存在性检查,AST → 逻辑计划
│
▼
[3] Logical Planner → 生成逻辑算子树(Scan / Filter / Join / Aggregate...)
│
▼
[4] Optimizer(优化器) → 谓词下推、列裁剪、常量折叠、Join 重排、子查询扁平化...
│
▼
[5] Physical Planner → 生成物理计划,确定具体算法(Hash Join / Merge Join)
│
▼
[6] Execution Engine → 向量化 + 多线程并行执行,Pipeline 驱动
│
▼
结果(列向量 → DataFrame / Arrow / 文件)
2.1 优化器做了什么(你该知道的几件事)
DuckDB 的优化器是"基于规则的 + 少量代价估算"的混合体,几个对你最关键的优化:
- 谓词下推(Predicate Pushdown):
SELECT * FROM t WHERE dt='2026-07-01' AND city='bj',过滤条件会被尽量推到 Scan 阶段,最早剔除无关数据,后面所有算子处理的数据量都变小。 - 列裁剪(Column Pruning):只读取 SQL 真正引用到的列。
- 分区裁剪(Partition Pruning):对按目录分区的 Parquet(如
s3://bucket/dt=2026-07-01/、dt=2026-07-02/),WHERE dt='2026-07-01'会直接跳过dt=2026-07-02的整个目录,连文件都不打开。 - 常量折叠 / 子查询扁平化:把能提前算的算掉,把相关子查询改写成 Join。
2.2 执行引擎:Pipeline + 多线程
DuckDB 的执行引擎把物理计划切分成若干 Pipeline(数据流动管道),每个 Pipeline 由多个算子串联,数据以"列向量批次"为单位在算子间流动。
并行方面,DuckDB 内置一个 TaskScheduler,根据你设置的线程数(默认等于 CPU 核数)把任务拆成小块并行处理。比如一个 SELECT SUM(x) FROM t,扫描和聚合会被分到多个线程,最后再合并。
-- 查看 / 设置并行线程数
PRAGMA threads; -- 查看当前线程数
PRAGMA threads=4; -- 限制为 4 线程(容器/受限环境常用)
2.3 存储层与事务
DuckDB 把数据以列式块(Column Data)持久化到单个 .duckdb 文件(或纯内存)。它支持 ACID,用 MVCC(多版本并发控制) 实现事务隔离;但注意,它的强项在"读多写少的分析负载",不是高并发写入。
压缩方面,DuckDB 对每列自动选择压缩方式:低基数列用字典编码(Dictionary Encoding),重复序列用 RLE(游程编码),整数经常用 位打包(Bit-Packing)。这就是为什么它能把 10GB 的原始 CSV 压成 1~2GB 的 DuckDB/Parquet 文件。
2.4 扩展系统(Extension)
DuckDB 内核精简,能力靠扩展补齐,常用内置/官方扩展:
| 扩展 | 作用 |
|---|---|
parquet | 读写 Parquet(核心,已内置) |
json | 读写 JSON(已内置) |
httpfs | 直读 S3 / GCS / HTTP 上的文件(湖仓关键) |
icu | 国际化排序/大小写 |
fts | 全文检索 |
spatial | 地理空间 |
arrow | Arrow 互操作 |
iceberg / delta | 湖仓表格式(新版逐步完善) |
INSTALL httpfs; -- 安装扩展(只需一次)
LOAD httpfs; -- 当前会话加载
三、代码实战:从三分钟上手到生产落地
光讲原理不过瘾,下面全部是能直接跑的代码。以 Python 为例(最常用),CLI 和 Go/Node 也会给片段。
3.1 三分钟上手
# 安装 Python 包
pip install duckdb
# 或者用官方 CLI(单文件二进制,零依赖)
# macOS: brew install duckdb
# 其他: 从 https://duckdb.org 下载对应平台二进制
import duckdb
# 1) 内存模式(不落盘)
con = duckdb.connect() # 不传路径 = 纯内存
con.execute("CREATE TABLE users(id INTEGER, name VARCHAR, age INTEGER)")
con.execute("INSERT INTO users VALUES (1,'alice',30),(2,'bob',25),(3,'carol',35)")
rows = con.execute("SELECT name, age FROM users WHERE age > 28 ORDER BY age DESC").fetchall()
print(rows) # [('carol', 35), ('alice', 30)]
# 2) 关系型 API(更 Pythonic,支持链式)
rel = duckdb.sql("SELECT age, COUNT(*) AS c FROM users GROUP BY age ORDER BY c DESC")
print(rel) # 直接打印表格
df = rel.df() # 转 pandas
print(df)
CLI 里同样丝滑:
duckdb # 进入交互式
D CREATE TABLE t(a INT, b INT);
D INSERT INTO t VALUES (1,2),(3,4);
D SELECT a+b AS s FROM t;
┌───────┐
│ s │
├───────┤
│ 3 │
│ 7 │
└───────┘
3.2 零拷贝直读文件:这是 DuckDB 的杀手锏
传统做法:先用 Pandas 把 CSV 读进内存 → 再查。DuckDB 直接把文件当表查,无需先加载:
import duckdb
# 直接查询单个 Parquet(列存、自带统计信息,扫描极快)
duckdb.sql("""
SELECT airline, AVG(delay) AS avg_delay, COUNT(*) AS n
FROM 'flights.parquet'
WHERE delay IS NOT NULL
GROUP BY airline
ORDER BY avg_delay DESC
LIMIT 5
""").show()
# 一次查一批文件(支持通配符 / 目录)
duckdb.sql("""
SELECT event, COUNT(*) AS cnt
FROM read_parquet('logs/2026-07-*.parquet')
GROUP BY event
""").show()
# CSV 自动推断 schema(read_csv_auto 非常聪明)
duckdb.sql("""
SELECT city, SUM(amount) AS total
FROM read_csv_auto('orders_*.csv', header=True)
GROUP BY city
""").show()
# JSON(支持嵌套、自动展开)
duckdb.sql("""
SELECT json_extract_string(payload, '$.user.name') AS name,
json_extract(payload, '$.amount') AS amount
FROM read_json_auto('events.jsonl', format='newline_delimited')
""").show()
关键点:read_parquet 会利用 Parquet 文件自带的 min/max/统计元数据做谓词下推。例如 WHERE dt='2026-07-01',DuckDB 在扫描前就读 Parquet 的 footer 统计,直接跳过不含该分区的 row group,IO 量可能减少 90% 以上。这是它比 Pandas pd.read_parquet 后再过滤快得多的重要原因——Pandas 是"全读进来再过滤",DuckDB 是"边读边过滤、只取需要的列"。
3.3 与 Pandas / Polars / Arrow 的互操作
DuckDB 与 Python 生态是"零摩擦"的:
import duckdb, pandas as pd
# Pandas → DuckDB(注册为虚拟表,默认零拷贝)
df = pd.DataFrame({'id':[1,2,3], 'v':[10,20,30]})
res = duckdb.sql("SELECT id, v*2 AS v2 FROM df WHERE v > 10").df()
print(res)
# id v2
# 0 2 40
# 1 3 60
# DuckDB → Pandas
pdf = duckdb.sql("SELECT range AS n FROM range(5)").df()
# DuckDB → Polars
try:
import polars as pl
pldf = duckdb.sql("SELECT range AS n, range*range AS sq FROM range(5)").pl()
except ImportError:
pass
# DuckDB ↔ Arrow(真正的零拷贝,适合大数据)
import pyarrow as pa
tbl = duckdb.sql("SELECT range AS n FROM range(1000000)").arrow()
print(type(tbl), tbl.num_rows) # <class 'pyarrow.Table'> 1000000
经验法则:数据已经在 pandas DataFrame 里 → 直接交给 DuckDB 算聚合/Join;要产出给下游 pandas/polars/arrow → 用 .df()/.pl()/.arrow() 拿回。DuckDB 的 C++ 引擎做重活,Python 只负责"指挥",绕开了 GIL 对计算的影响。
3.4 内存计算性能实测(对比 Pandas)
下面这段脚本你能在自己机器上复现,直观感受差距:
import duckdb, pandas as pd, numpy as np, time
N = 50_000_000
df = pd.DataFrame({
'user_id': np.random.randint(0, 1_000_000, N),
'event' : np.random.choice(['click','view','buy'], N),
'value' : np.random.rand(N) * 100,
})
# --- Pandas 方案 ---
t0 = time.time()
pandas_res = (df.groupby('event')['value'].agg(['sum','mean','count']))
pandas_cost = time.time() - t0
# --- DuckDB 方案(把 DataFrame 直接当表)---
t0 = time.time()
duck_res = duckdb.sql("""
SELECT event, SUM(value) AS s, AVG(value) AS m, COUNT(*) AS c
FROM df
GROUP BY event
""").df()
duck_cost = time.time() - t0
print(f"Pandas: {pandas_cost:.2f}s | DuckDB: {duck_cost:.2f}s | 提速 {pandas_cost/duck_cost:.1f}x")
在我本地(Apple M 系列、8 线程)跑 5000 万行聚合,DuckDB 通常比等效 Pandas 快 5~15 倍,且内存占用显著更低——因为 DuckDB 是流式、批处理、列式,不会把整张宽表同时物化成 Python 对象。Pandas 的 groupby 在大数据下不仅慢,还容易把内存吃爆。
3.5 湖仓实战:直接查对象存储上的 Parquet(httpfs)
这是 DuckDB 真正"重塑数据工程"的能力——你不需要把数据搬下来,直接查 S3 / GCS / 本地 Hive 分区目录里的 Parquet:
import duckdb
con = duckdb.connect()
con.execute("INSTALL httpfs; LOAD httpfs;")
# 配置 S3(按需填你的凭证;也可用 ~/.aws/credentials)
con.execute("SET s3_region='us-east-1';")
con.execute("SET s3_access_key_id='YOUR_KEY';")
con.execute("SET s3_secret_access_key='YOUR_SECRET';")
# 直接 SELECT 云端 Parquet,谓词下推 + 列裁剪在远端文件上生效
sql = """
SELECT date, SUM(revenue) AS daily_revenue
FROM read_parquet('s3://my-bucket/sales/dt=*/part-*.parquet')
WHERE date >= DATE '2026-01-01'
GROUP BY date
ORDER BY date
"""
con.sql(sql).show()
# 甚至可以把结果直接写回对象存储(ETL 闭环)
con.execute("""
COPY (
SELECT date, SUM(revenue) AS daily_revenue
FROM read_parquet('s3://my-bucket/sales/dt=*/part-*.parquet')
GROUP BY date
) TO 's3://my-bucket/agg/daily_revenue.parquet' (FORMAT PARQUET);
""")
注意两件事:
dt=*这种 Hive 分区目录,DuckDB 会把它识别成dt列,并且WHERE dt='...'能触发分区裁剪——不匹配的分区目录根本不下载。- 谓词下推对 远端 Parquet 同样有效。它会在扫描阶段就利用 Parquet 的 row-group 统计信息跳过无关数据块,只下载需要的列和行组,省下的网络和存储 IO 非常可观。
3.6 生产级 ETL:用 DuckDB 替代 Spark 清洗轨迹数据
真实案例:某出行平台用 Spark 跑司机轨迹清洗,单任务 38 分钟、集群成本高。改用 DuckDB 单机 pipeline 后,同样的数据量 3 分钟内完成(数据量在单机可承载范围内时)。核心代码骨架:
import duckdb
con = duckdb.connect()
con.execute("PRAGMA threads=8;")
con.execute("PRAGMA memory_limit='8GB';") # 限制内存,溢出到 temp
con.execute("PRAGMA temp_directory='/tmp/duckdb_tmp';")
# 1) 读取原始多文件轨迹(Parquet 目录 + 分区)
con.execute("""
CREATE VIEW raw_trips AS
SELECT * FROM read_parquet('s3://telematics/trips/dt=2026-07-*/trip.parquet');
""")
# 2) 清洗:去噪、过滤异常点、补全城市
con.execute("""
CREATE TABLE clean_trips AS
SELECT
trip_id,
driver_id,
city,
ST_Distance( -- spatial 扩展
ST_Point(pickup_lon, pickup_lat),
ST_Point(dropoff_lon, dropoff_lat)
) AS straight_km,
(dropoff_ts - pickup_ts) / 1000.0 AS duration_s,
distance_m / NULLIF((dropoff_ts - pickup_ts)/1000.0, 0) AS avg_speed
FROM raw_trips
WHERE pickup_lat BETWEEN -90 AND 90
AND dropoff_lat BETWEEN -90 AND 90
AND distance_m > 50 -- 过滤噪点
AND (dropoff_ts - pickup_ts) > 0 -- 时间必须正向
""")
# 3) 聚合指标,写出供 BI 消费
con.execute("""
COPY (
SELECT city,
DATE_TRUNC('hour', to_timestamp(pickup_ts/1000)) AS hour,
COUNT(*) AS trips,
AVG(duration_s) AS avg_duration,
AVG(avg_speed) AS avg_speed
FROM clean_trips
GROUP BY city, hour
) TO 's3://telematics/agg/hourly_city.parquet' (FORMAT PARQUET, PARTITION_BY (city));
""")
print("ETL done.")
要点:
CREATE TABLE ... AS SELECT(CTAS)把中间结果物化成 DuckDB 表,避免重复扫描;PRAGMA memory_limit+temp_directory让超内存数据溢出到磁盘,避免 OOM;- 最后
COPY ... TO ... PARTITION_BY直接按城市分区写出 Parquet,BI 工具(如 DuckDB / Supabase / 任意支持 Parquet 的系统)可直接消费。
何时该用 DuckDB,何时该回到 Spark? 经验线:单节点能装下的数据(几十 GB 到几百 GB,取决于内存与是否允许磁盘溢出)、追求开发效率和低运维成本 → DuckDB。数据量真正到 TB 级、需要分布式容错和弹性伸缩 → Spark / 云数仓仍是更稳妥的选择。DuckDB 不是要"干掉" Spark,而是吃掉 Spark 被过度使用的中小数据场景。
3.7 把 DuckDB 嵌进你的应用(Go / Node / Rust)
DuckDB 不止能当 Python 脚本工具,也能做成服务的"内置分析模块"。
Node.js:
const duckdb = require('duckdb');
const db = new duckdb.Database(':memory:');
db.all("SELECT range AS n, range*range AS sq FROM range(5)", (err, rows) => {
if (err) throw err;
console.log(rows);
});
Go(使用 duckdb-go 绑定):
package main
import (
"database/sql"
"fmt"
_ "github.com/marcboeker/go-duckdb"
)
func main() {
db, _ := sql.Open("duckdb", "")
defer db.Close()
rows, _ := db.Query("SELECT range AS n FROM range(3)")
defer rows.Close()
for rows.Next() {
var n int
rows.Scan(&n)
fmt.Println(n)
}
}
典型用法:一个 Web 服务后端,用户上传一个 CSV/Parquet,你想在不引入数据分析栈的前提下给它做即时聚合预览 → 直接用 DuckDB 在进程内查,毫秒级返回。
3.8 进阶扩展:全文检索与时间序列
DuckDB 还能干一些"非主流但很香"的活:
-- 全文检索(fts 扩展)
INSTALL fts; LOAD fts;
CREATE TABLE docs(id INTEGER, body VARCHAR);
INSERT INTO docs VALUES (1,'DuckDB is a fast in-process OLAP database'),
(2,'PostgreSQL is a powerful transactional database');
PRAGMA create_fts_index('docs', 'id', 'body');
SELECT id, body FROM docs WHERE fts_main_docs.match_bm25(id, 'OLAP database');
-- 时间序列(DATE_TRUNC / 窗口函数)
SELECT
DATE_TRUNC('day', ts) AS day,
SUM(amount) AS daily,
AVG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('day', ts)
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_7d
FROM events
GROUP BY day
ORDER BY day;
四、性能优化:为什么快,以及如何更快
理解了原理,优化就有章法了。下面是实战中最高频的调优点。
4.1 永远优先"列存 + 直读文件",别先 INSERT
最大误区:把 CSV 用 INSERT 一行行塞进 DuckDB 表。正确姿势是直接 CREATE TABLE ... AS SELECT * FROM read_parquet(...) 或 COPY ... TO 'x.parquet'。DuckDB 对 Parquet 的读取/写入做了大量优化,比"行级 INSERT"快几个数量级。
4.2 让过滤条件"尽量早、尽量靠近 Scan"
优化器会帮你下推谓词,但你能做的是:把过滤写在最内层子查询 / CTE 里,而不是最后 SELECT 才 WHERE。这样中间结果集更小,后续 Join/聚合更轻。
-- 推荐:先过滤再 Join
WITH hot AS (
SELECT * FROM events WHERE dt = DATE '2026-07-01' -- 分区裁剪 + 列裁剪
)
SELECT h.user_id, u.name
FROM hot h JOIN users u ON h.user_id = u.id;
4.3 分区目录用 Hive 风格,触发分区裁剪
把数据按 dt=2026-07-01/ 这样的目录组织,查询时 WHERE dt='2026-07-01' 会跳过其他日期目录。对 TB 级冷数据,这能省掉 99% 的扫描量。
4.4 控制线程与内存,避免"抢满整机"
在容器 / 共享机器上,务必显式设置:
PRAGMA threads=4; -- 别默认吃满所有核,留给系统和其他进程
PRAGMA memory_limit='4GB'; -- 限制内存上限
PRAGMA temp_directory='/tmp/duckdb_tmp'; -- 超内存时溢出目录(必须可写)
4.5 批量写出用 COPY,别用循环 INSERT
-- 正确:一次性向量化写出
COPY (SELECT * FROM big_result) TO 'out.parquet' (FORMAT PARQUET);
-- 错误:逐行 INSERT,慢且无意义
-- INSERT INTO out VALUES (...); -- 在循环里
4.6 常见坑位提醒
- 类型推断陷阱:
read_csv_auto对列类型可能推断错(如把0012当整数丢掉前导零)。遇到脏数据用read_csv+ 显式columns={'code':'VARCHAR'}指定类型。 - Python GIL:DuckDB 的重计算在 C++ 层,不怎么受 GIL 影响;但把超大结果
.df()回传 pandas 时,物化那一步会吃内存,建议用.arrow()或在 SQL 里先聚合再取。 - 不要拿它当高并发写入库:DuckDB 单写者模型,高并发
INSERT不是它的强项。写多读少、高并发交易 → 还是 PostgreSQL / MySQL。 - 大文件流式:能用
read_parquet流式就不要先CREATE TABLE ... AS全量物化,除非后续要反复查。
五、总结与展望
5.1 决策树:我该用 DuckDB 吗?
你需要做分析型查询(聚合 / Join / 扫描大文件)吗?
├─ 否(高并发事务、CRUD、点查为主)→ PostgreSQL / MySQL / SQLite
└─ 是
├─ 数据在单机可承载范围(几十 GB ~ 数百 GB,可磁盘溢出)?
│ ├─ 是 → DuckDB(本地分析 / 嵌入式 / ETL / 数据App内置分析)✅
│ └─ 否(TB 级、需分布式容错弹性)→ Spark / ClickHouse / 云数仓
└─ 需要跨进程、多客户端共享一个分析服务?
├─ 是 → ClickHouse / 云数仓
└─ 否 → DuckDB
5.2 横评一览
| 维度 | DuckDB | SQLite | Pandas | ClickHouse | Spark |
|---|---|---|---|---|---|
| 定位 | 嵌入式 OLAP | 嵌入式 OLTP | 内存 DataFrame | 服务端 OLAP | 分布式计算 |
| 存储 | 列式 | 行式 | 行式(内存) | 列式 | 多格式 |
| 部署 | 零(库) | 零(库) | 零(库) | 需服务 | 需集群 |
| 大数据 | 中(单机+溢出) | 弱 | 弱(OOM) | 强 | 最强 |
| 并发写 | 弱(单写者) | 强 | 无 | 强 | 强 |
| 直读 Parquet/S3 | ✅ 原生 | ❌ | 需库 | 部分 | ✅ |
| 适用 | 本地分析/ETL/嵌入式 | 嵌入式事务 | 小数据探索 | 服务化分析 | 超大规模 |
5.3 2026 生态观察
- MotherDuck:DuckDB 的云化版本,把本地 DuckDB 与云端共享存储打通,支持"本地算 + 云上协作",让单机引擎也能做团队级分析。
- 湖仓表格式支持增强:对 Iceberg / Delta 的读取支持在持续完善,"DuckDB 直接当 Iceberg 查询引擎"正变成现实。
- 与 Python 数据栈深度绑定:pandas / polars / arrow / 各类 notebook 里,DuckDB 正成为"默认分析后端"之一。
- AI / RAG 场景:向量检索、语义缓存之外,DuckDB 也被用作本地知识库的结构化存储与查询层(配合内嵌扩展)。
5.4 结语
DuckDB 给我们的启示其实很朴素:很多"大数据问题",其实只是"中数据问题"被错误的工具链复杂化了。在 90% 的场景里,你不需要一个分布式集群,你需要的是一个"能直接打开文件、用 SQL 飞快算完、用完即走"的引擎。
它不会取代 Spark,也不会取代 PostgreSQL。但它会取代你电脑上那一堆"为了分析个 CSV 而临时起的 Spark 任务""为了 GROUP BY 而硬撑的 Pandas 进程""为了查个 Parquet 而登录的云控制台"。
下次再有人问"这个 30GB 的日志怎么快速算出 TOP 10",别急着开集群——先 duckdb 打开它,写一条 SQL,然后去喝杯咖啡。等咖啡凉了,它大概率早就跑完了。
延伸练习:把你手头任意一个超过 1GB 的 CSV / Parquet 丢给
duckdb,对比pandas.read_csv().groupby().agg()的耗时与内存占用,你会更直观地理解本文说的"向量化 + 列式"到底意味着什么。