DuckDB 深度拆解:当「分析界的 SQLite」装上湖仓引擎——从向量化执行、VARIANT 类型到 DuckLake 三层架构革命(2026)
关键词:DuckDB、进程内分析、向量化执行、列式存储、VARIANT、DuckLake、Lakehouse、Parquet、OLAP
适用读者:每天和数据打交道的后端 / 数据工程师,以及被「数据不大却怎么都跑不快」折磨过的程序员。
一、背景:我们到底缺一个什么样的数据库
先讲一个几乎每个工程师都踩过的坑。
你手上有一份 30 GB 的 Parquet 日志,可能放在本地磁盘,也可能躺在 S3 上。你想回答一个其实很简单的问题:过去 90 天,每个地区的错误率趋势是什么?
传统路径有三套:
- Pandas / Polars:把数据读进内存。30 GB 大概率直接 OOM,或者勉强跑起来后你的笔记本风扇像起飞。
- PostgreSQL / MySQL:先把 30 GB 导进去。建表、建索引、等导入,导完发现分析查询依然很慢——毕竟它们是行存 OLTP 数据库,不是为了扫描而生的。
- Spark / 数据仓库:为了查一个 30 GB 的文件,你要拉起一个分布式集群,写一堆配置,等十分钟只是为了拿到第一行结果。
问题的本质在于:绝大多数真实世界的数据分析,处在「内存装不下、又不值得上集群」的中间地带(业内常叫 medium data,约 1 GB ~ 1 TB)。这个区间里,我们缺一个像 SQLite 那样「零依赖、单文件、拿起来就用」、但专门为分析(OLAP)设计的东西。
DuckDB 的 slogan 很直白:"The SQLite for Analytics"。它把 SQLite 在 OLTP 领域的成功范式(进程内、嵌入式、零配置、单文件)搬到了分析领域,并用列式存储 + 向量化执行把扫描性能拉满。
1.1 时间线:一只从 CWI 实验室游出来的鸭子
- 2018:Mark Raasveldt 与 Hannes Mühleisen 在荷兰 CWI 研究所启动项目,目标是做一个嵌入式分析引擎。
- 2021:开源,凭借「直接查 Parquet / CSV」的能力在 data 圈一炮而红。
- 2024-06:发布 1.0.0 "Nivis",正式承诺存储格式与查询语义稳定。
- 2025-09:发布 1.4.0 LTS "Andium",引入商业级特性(数据库加密、MERGE、Iceberg 写)。从这一版起,偶数版本为 LTS,提供一年社区支持。
- 2026-03-09:发布 1.5.0 "Variegata"(天堂鸭),6500+ commits、近百位贡献者,带来全新 CLI、VARIANT 类型、内置 GEOMETRY,以及 DuckLake 规范的定型。
- 2026-06-17:发布 1.5.4(最新补丁),同时 1.4.5 LTS 继续维护到 2026-09。
- 2026-09(计划):DuckDB 2.0 大版本。
值得一提的是,DuckDB 背后有 DuckDB Labs 与云服务 MotherDuck(由 DuckDB 原作者创立),前者负责内核与商业支持,后者提供「云端 + 边缘协同」的托管分析。本文聚焦内核能力,但会在第五节提到 MotherDuck 带来的 client-server 可能性。
1.2 一句话定位
DuckDB 不是来替代 PostgreSQL、ClickHouse 或 Spark 的。它是你本地、进程内、随用随走的「分析第一公民」——当你需要快、准、零配置地分析一份文件或一个小数据集时,它是默认选项。
二、核心概念:DuckDB 到底「是什么」
理解 DuckDB,先把它和几个常见系统摆在一起:
| 维度 | SQLite | DuckDB | PostgreSQL | ClickHouse | Spark |
|---|---|---|---|---|---|
| 定位 | OLTP | OLAP | 通用(偏 OLTP) | OLAP(服务端) | 分布式批处理 |
| 存储 | 行存 | 列存 | 行存(可列存扩展) | 列存 | 多格式 |
| 部署 | 进程内 | 进程内 | 客户端/服务端 | 客户端/服务端 | 集群 |
| 依赖 | 单文件 | 单文件 / 单库 | 服务进程 | 服务进程 | JVM 集群 |
| 典型数据量 | MB | GB ~ 数百 GB | GB ~ TB | TB ~ PB | TB ~ PB |
| 查询文件 | 否 | 直接查 Parquet/CSV/JSON | 需导入 | 需导入/外部表 | 需登记 |
几个核心概念一定要吃透:
2.1 进程内(In-Process)
DuckDB 没有「服务端」。它被编译成一个动态库(.so / .dylib / .dll)或一个 CLI 二进制。Python 里 import duckdb 之后,查询就在你的进程里跑,数据和计算之间没有网络往返、没有序列化开销。这和 SQLite 的哲学一致:数据库是你应用的一部分,不是你要去连接的一个外部系统。
2.2 列式存储(Columnar)
分析查询通常是「对少数几列做聚合,扫很多行」(如 SELECT sum(amount) FROM sales WHERE region='APAC')。列存让数据库只读取用到的列,并对每列独立压缩——一列里的值往往高度相似,压缩比极高。
2.3 向量化执行(Vectorized Execution)
这是 DuckDB 性能的根本来源,下一节专门拆。
2.4 直接查询外部文件
不需要 CREATE TABLE + INSERT,你可以直接对 Parquet / CSV / JSON 跑 SQL:
-- 直接扫一个 Parquet 目录,自动识别 schema、自动并行
SELECT region, count(*) AS n, avg(latency_ms) AS p_latency
FROM 'logs/2026-*.parquet'
WHERE status >= 500
GROUP BY region
ORDER BY n DESC;
这一条特性,让 DuckDB 成了数据工程师手里性价比最高的「瑞士军刀」。
三、架构分析:向量化执行引擎的内部
很多文章把「向量化」当成口号,这里我们把引擎真的拆开看。
3.1 执行单元不是「一行」,而是一个 Vector
传统 Volcano 模型(PostgreSQL 用的)是「一次给上层吐一行」,每行的每个算子都要一次虚函数调用(迭代器 next())。当一行只有几十字节、你要扫十亿行时,函数调用本身的开销就吃掉了一大半 CPU。
DuckDB 改用 向量化 Volcano 模型:每个算子一次处理一个 Vector——默认 2048 行的批量列数据(STANDARD_VECTOR_SIZE)。
算子 A (扫描) ──Vector(2048行)──▶ 算子 B (过滤) ──Vector──▶ 算子 C (聚合)
收益是结构性的:
- 减少虚函数调用 2048 倍:从「每行的 next()」变成「每 2048 行的 next()」。
- CPU 分支预测更友好:连续处理同类数据,流水线不易被打断。
- 天然适配 SIMD:对 2048 个 double 做
+=,编译器很容易生成向量指令。
3.2 流水线 + Morsel-Driven 并行
DuckDB 把查询拆成多条 Pipeline(如「扫描+过滤」一条,「聚合」一条),每条 Pipeline 内部按 morsel(数据小块)调度到线程池。每个线程认领一个 morsel 跑完整个局部流水线,再由最后的汇聚算子合并。这让多核被充分利用,且不需要你写任何并行代码。
-- 看看并行度:默认用满所有核
SELECT current_setting('threads');
-- 临时降为单线程做对照测试
SET threads = 1;
3.3 压缩感知执行(Compression-Aware)
DuckDB 在列存上用轻量压缩:常量段、RLE、字典、位打包(bit-packing)等。关键是很多算子能直接在压缩数据上算,不必先解压。例如对一个「字典编码」的列做 count(distinct),可以直接在字典上算;对 RLE 列做 sum,直接 值 × 重复次数。
实战提示:从 1.4 起,内存表也支持压缩(默认关)。开启后某些查询有 5~10 倍提升:
ATTACH ':memory:' AS mem_compressed (COMPRESS); USE mem_compressed;
3.4 DuckDB vs ClickHouse:都是向量化,差别在哪
两者都用向量化 + 列存,但场景不同:
- ClickHouse 是服务端、可分布式、为 PB 级与高并发点查设计,需要部署、建表、运维。
- DuckDB 是进程内、单节点、为「在你的笔记本 / 函数 / 容器里秒级分析一份文件」设计,零运维。
它们不是替代关系,而是互补:数据在湖里用 ClickHouse 扛大流量,数据科学家在本地用 DuckDB 做探索和原型,二者都能直接读写 Parquet / Iceberg,格式互通。
3.5 VARIANT 类型:半结构化数据的「正解」
1.5 引入的 VARIANT 类型,是本文最值得展开的新特性。它受 Snowflake 启发,也对应 2025 年写入 Parquet 规范的 VARIANT 编码。
过去 DuckDB 用 JSON 类型存半结构化数据,但 JSON 在物理上是纯文本——每次查询都要解析字符串,慢且臃肿。
VARIANT 把每个值以带类型的二进制存储,每一行自带类型信息:
CREATE TABLE events(id INTEGER, data VARIANT);
INSERT INTO events VALUES
(1, 42::VARIANT),
(2, 'hello world'::VARIANT),
(3, [1, 2, 3]::VARIANT),
(4, {'name': 'Alice', 'age': 30}::VARIANT);
-- 每一行都能看到自己的真实类型
SELECT id, variant_typeof(data) AS vtype FROM events;
-- 1 -> INT32, 2 -> VARCHAR, 3 -> ARRAY(3), 4 -> OBJECT(name, age)
-- 用点号直接取嵌套字段
SELECT data.name FROM events WHERE id = 4; -- Alice
为什么这很重要?因为真实世界的事件流(埋点、IoT、Webhook)几乎都是半结构化 JSON。VARIANT 让你既能保留 schema 灵活性,又能拿到接近结构化列的查询速度与压缩比,还能直接读写带 VARIANT 的 Parquet 文件(含 shredding——把嵌套结构拆成扁平列)。
3.6 存储格式、事务与加密
DuckDB 把整个数据库存进单个文件,带 WAL(写前日志)和 checkpoint,提供 ACID。从 1.4 起支持 AES-256-GCM 加密,覆盖主库文件、WAL 甚至临时文件:
ATTACH 'secret.duckdb' AS sec (ENCRYPTION_KEY 'quack_quack');
-- 需要硬件加速时先 LOAD httpfs(OpenSSL 后端更快)
LOAD httpfs;
四、代码实战:从安装到 DuckLake
4.1 安装:一条命令或一个 import
# CLI(macOS / Linux)
curl https://install.duckdb.org | bash
# 或用包管理器
brew install duckdb
# Python
pip install duckdb
# Node.js
npm install duckdb
# Go
go get github.com/marcboeker/go-duckdb
# Rust
cargo add duckdb
4.2 Python:像写 Pandas,跑出 C 的速度
DuckDB 的 Python API 设计得极「Pythonic」,能零拷贝地和 Pandas DataFrame、Arrow Table 互转:
import duckdb
import pandas as pd
# 1) 直接对文件跑 SQL,返回 Relation(惰性,未真正执行)
rel = duckdb.sql("""
SELECT airline,
count(*) AS flights,
avg(dep_delay) AS avg_delay,
quantile_cont(dep_delay, 0.99) AS p99_delay
FROM 's3://duckdb-benchmark/ontime/*.parquet'
WHERE dep_delay > 0
GROUP BY airline
ORDER BY avg_delay DESC
LIMIT 5
""")
# 2) 转成 DataFrame / Arrow 继续用熟悉的工具
df: pd.DataFrame = rel.df()
print(df.head())
# 3) 反过来,用 SQL 查一个已有的 DataFrame(自动零拷贝注册)
trades = pd.DataFrame({"sym": ["AAPL", "AAPL", "TSLA"], "px": [190, 191, 240]})
out = duckdb.sql("SELECT sym, avg(px) AS avg_px FROM trades GROUP BY sym").df()
print(out)
关键点:duckdb.sql(...) 返回的是 Relation(惰性计划),真正物化发生在你调用 .df() / .arrow() / .fetchall() 时。这让你可以把多个步骤拼成一条执行计划,DuckDB 会整体优化,而不是一步步把中间结果拷来拷去。
4.3 直接吃外部文件:Parquet / CSV / JSON
-- 单个文件
SELECT * FROM 'data/orders.parquet' LIMIT 10;
-- 通配符 + 自动分区裁剪(按路径里的分区列过滤,只读需要的文件)
SELECT * FROM 'data/dt=2026-07-*/region=APAC/*.parquet'
WHERE dt BETWEEN '2026-07-01' AND '2026-07-15';
-- 读 CSV,自动探测类型
SELECT * FROM read_csv_auto('data/events.csv', header=true);
-- 读 JSON( newline-delimited 或自动展开)
SELECT * FROM read_json_auto('data/clicks.ndjson');
DuckDB 在读 Parquet 时会做 投影下推(只读 SELECT 里的列)和谓词下推(把 WHERE 推到扫描层,提前过滤),所以「查 30 GB 文件里两列」几乎是秒级。
4.4 ATTACH / COPY / MERGE:把 DuckDB 当引擎用
-- 挂载另一个 DuckDB 文件,像跨库 JOIN
ATTACH 'warehouse.duckdb' AS wh;
SELECT o.id, o.amount, c.name
FROM wh.orders o
JOIN customers c ON o.cust_id = c.id;
-- 把查询结果直接写成 Parquet(可写本地 / S3 / Azure)
COPY (SELECT * FROM wh.orders WHERE amount > 1000)
TO 's3://my-bucket/big_orders.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
-- MERGE:UPSERT,无需主键,按任意条件合并(1.4+)
CREATE TABLE stock(item_id INTEGER, balance INTEGER);
INSERT INTO stock VALUES (10, 2200), (20, 1900);
WITH incoming(item_id, volume) AS (VALUES (20, 2200), (30, 1900))
MERGE INTO stock
USING incoming USING (item_id)
WHEN MATCHED THEN UPDATE SET balance = balance + volume
WHEN NOT MATCHED THEN INSERT VALUES (item_id, volume)
RETURNING merge_action, *;
-- 返回:UPDATE (20 -> 4100), INSERT (30 -> 1900)
MERGE 是 OLAP 系统里极常用的「增量更新」原语,过去 DuckDB 只能靠 INSERT ... ON CONFLICT 且要求主键,现在对任意合并条件都支持,还能 DELETE。
4.5 全新 CLI:从「能用」到「好用」
1.5 重写了命令行客户端,体验直线上升:
- 动态提示:实时显示当前库 / schema(
memory D→my_db.my_schema D)。 - 彩色输出 + 分页器:结果超 50 行自动进 pager。
_引用上一条结果:不用重跑长查询就能继续加工。.tables/DESCRIBE:像 psql 一样看库结构。- PEG 解析器(实验):
CALL enable_peg_parser();后,TAB 补全和报错信息大幅改善(为 2.0 全面切换做准备)。
duckdb
memory D ATTACH 'https://blobs.duckdb.org/data/animals.db' AS a;
a D FROM ducks WHERE extinct_year IS NOT NULL;
a D FROM _; -- 引用上一条结果
4.6 DuckLake:用「一个 SQL 数据库」管元数据,重写 Lakehouse
这是 2026 年最值得关注的方向之一。Lakehouse(湖仓一体)想兼顾「对象存储的开放性」和「数据仓库的事务一致性」,于是有了 Iceberg、Delta Lake。但它们都绕不开一个痛点:在一致性弱的 S3 上做 ACID 极难——原子地切换「表的最新版本指针」、管理多表、处理小文件更新,要么依赖脆弱的 JSON/Avro 元数据,要么再叠一层 catalog 服务。
DuckLake 的核心洞察极其朴素:既然最后总要引入一个数据库来管理元数据,不如从一开始就把元数据放进一个标准的 SQL 数据库里(SQLite / PostgreSQL / DuckDB 本身)。 数据仍然用 Parquet 存在对象存储,但所有「表在哪、版本几、谁删了哪几行」都由这个元数据库用标准 SQL 事务保证。
-- 安装并挂载一个 DuckLake(元数据用本地 DuckDB 文件,数据落 S3)
INSTALL ducklake;
LOAD ducklake;
ATTACH 'ducklake:meta.duckdb' AS my_lake (
DATA_PATH 's3://my-bucket/lake/'
);
USE my_lake;
CREATE SCHEMA IF NOT EXISTS main;
CREATE TABLE main.sales(
ts TIMESTAMP,
region VARCHAR,
amount DOUBLE
);
INSERT INTO main.sales
SELECT now(), 'APAC', 123.45;
-- 之后的查询和普通的 DuckDB 表一模一样
SELECT region, sum(amount) FROM main.sales GROUP BY region;
DuckLake 的三层结构:
| 层 | 职责 | 技术 |
|---|---|---|
| 存储层 | 存真实数据 | 对象存储(S3/ABS/GCS)上的 Parquet |
| 元数据层 | 管 schema、版本、事务 | 一个标准 SQL 数据库(DuckLake catalog) |
| 计算层 | 跑查询 | DuckDB(本地)或 MotherDuck(云端) |
它直接解决了数据湖的「小改动难题」:要 UPDATE 一行,传统方案要在对象存储上写一堆 manifest;DuckLake 只要在元数据库里发一条事务。DuckLake 规范 v1.0 于 2026 年 4 月发布,1.5 已对其对齐(含宏、排序表、删除内联等)。
五、性能优化:让 DuckDB 跑出该有的速度
DuckDB 已经很快,但用对姿势能再快一个数量级。
5.1 第一原则:别把数据导进内存再算
错误姿势:pd.read_parquet(...) 然后 Pandas 慢慢 groupby。正确姿势:让 DuckDB 直接吃文件,把过滤、聚合、投影全部下推到引擎里。DuckDB 的 Parquet 扫描能跑满内存带宽(GB/s 级)。
5.2 分区与文件布局
按常用过滤字段(日期、地区)做目录分区:data/dt=2026-07-01/region=APAC/part-0.parquet。DuckDB 会做分区裁剪,只打开命中的文件。一个 1 TB 的湖,按天分区后查「昨天」可能只读几十 GB。
5.3 投影下推与列选择
只 SELECT 用到的列。列存 + 投影下推意味着「读 2 列」和「读 200 列」的 IO 差 100 倍。
5.4 并行度
-- 默认 threads = 核数;超大查询可调高,但别超过物理核
SET threads = 8;
-- 内存上限(默认物理内存 80%)
SET memory_limit = '8GB';
5.5 CTE 物化与执行计划
从 1.4 起,CTE 默认物化(而非内联),复杂复用查询更快也更稳。想看计划:
EXPLAIN ANALYZE
SELECT region, sum(amount) FROM main.sales GROUP BY region;
5.6 什么时候「不要用」DuckDB
诚实地说边界:
- 高并发写 / 多写者:DuckDB 是单写者(MVCC,读不阻塞写、写不阻塞读,但同一时刻只有一个 writer)。多服务并发写请用 PostgreSQL / 专用 OLTP。
- 超 PB 级、需要横向扩展:单节点有上限,上 ClickHouse / Spark / 云数仓。
- 需要强事务型业务系统:订单、账户这类请交给 OLTP 数据库。
DuckDB 的甜区是:单机、分析型、文件 / 中等规模数据。它和 Postgres(事务)、ClickHouse(大规模 OLAP)、Spark(分布式)是「组合拳」,不是二选一。
5.7 云端协同:MotherDuck 与 client-server
DuckDB 本体是进程内,但通过 MotherDuck 可以把同一个数据库在「本地进程」和「云端」之间无缝切换:本地做探索,云端做共享与更大算力的查询,二者共享 DuckDB 文件格式。这正好顺应了 2026 年「本地优先(local-first)+ 云端协同」的数据架构趋势。
六、总结与展望
把全文串起来,DuckDB 的工程设计可以收敛成三句话:
- 用 SQLite 的方式做分析:零依赖、单文件、进程内,拿起来就能查一份文件。
- 用向量化 + 列存把扫描拉满:一次处理 2048 行,压缩感知、多核并行,medium data 区间吊打 Pandas 和单机 Postgres。
- 用 DuckLake 把分析延伸到湖仓:元数据放进标准 SQL 数据库,ACID 在对象存储上不再是玄学,本地和云端共享同一套 Parquet。
对工程师的实操建议:
- 新项目里,凡是「读文件做分析」的环节,默认先想到 DuckDB。CLI 一行命令就能摸清一份陌生 Parquet 的 schema 和分布。
- ETL / 数据质量 / 探索性分析用 DuckDB 当引擎,结果落 Parquet / Iceberg,再喂给下游。
- VARIANT 处理半结构化日志时,比
JSON文本类型又快又省,且能和 Parquet 的 VARIANT 编码互通。 - 需要事务型或多写者时,老老实实用 PostgreSQL;需要 PB 级高并发时,上 ClickHouse。DuckDB 是你的「分析瑞士军刀」,不是银弹。
展望:DuckDB 2.0 预计 2026 年 9 月,会带来 PEG 解析器全面切换、lambda 旧语法移除、空间函数轴序默认修正等里程碑式清理;DuckLake 也在快速逼近 v1.0 正式规范。「SQL 无处不在、分析随用随走」的范式,正被这只鸭子一点点坐实。
最后一句大实话:下次再有人让你「把这份 20 GB 的 CSV 统计一下」,别开 Pandas 了,敲一行
duckdb -c "SELECT ... FROM 'big.csv'"然后去泡杯咖啡——回来结果已经在屏幕上了。
参考资料:DuckDB 官方博客(1.4 LTS / 1.5 Variegata 发布公告)、DuckLake 规范文档、DuckDB 文档站。本文代码示例均基于 DuckDB 1.5.x,部分特性需 1.4+。