DuckDB 深度拆解:向量化执行引擎 + Async I/O「Work, Thread, Work」——嵌入式 OLAP 如何重写本地数据分析范式(2026 实战指南)
当你需要跑一个几 GB 的聚合查询,却不想为了这点事部署一套 ClickHouse 或 Spark;当 Pandas 在你的 16G 内存笔记本上处理 8G CSV 直接 OOM;当 Excel 打开一个稍大的文件就转圈圈——你会发现,数据分析的「中间地带」长期是空白的。DuckDB 正是为这个地带而生:它被称为「分析领域的 SQLite」。本文从背景、核心概念、执行引擎架构、Async I/O 革命、多语言代码实战到 15 条生产级性能建议,把 DuckDB 拆透。
一、背景介绍:数据分析的「中间地带」困境
我们早就有了清晰的两极。
一端是 OLTP 的轻量之王 SQLite。 一个文件、零配置、嵌入进程,App 里存点本地数据、缓存、配置,几乎没人会再纠结。它解决了「我不想为了存 100 条记录起一个数据库服务」的问题。
另一端是重量级 OLAP 引擎。 ClickHouse、Doris、Spark、Snowflake、BigQuery——它们解决的是「PB 级数据、上百节点、多团队共享」的问题。代价是:你要有一套集群、一套运维、一套权限,部署成本以「人周」计。
但现实里大量工作卡在中间:
- 数据分析师拿到一份 5GB 的 Parquet,只想
GROUP BY一下看分布; - 后端工程师想在应用里直接对一批落盘日志做即席聚合,又不想引入消息队列 + 数据仓库;
- 数据工程师做 ETL,中间结果就是几个文件,却要为了查一下而把数据导进一个「正规」数据库;
- 科研人员处理实验产出的 CSV,Pandas 读进内存就爆了。
这些场景的共同点是:数据量超过内存舒适区,但远没到需要分布式系统的程度;你需要的只是一个能嵌进进程、懂列式、会向量化、能直接读文件的 SQL 引擎。
DuckDB 就是来填这个空白的。它由荷兰 CWI(Centrum Wiskunde & Informatica,MonetDB 的诞生地)的团队主导,核心作者 Mark Raasveldt 与 Hannes Mühleisen 身上带着浓厚的「列式数据库」学术血统。2026 年 8 月,DuckDB 在 GitHub 上突破了 40,000 颗 Star,最新稳定版 1.5.5(2026-07-22 发布)持续在向量化执行与 I/O 模型上做深挖——其中 2026-07-31 由 Pedro Holanda 发布的 「Asynchronous I/O in DuckDB: Work, Thread, Work」 深度文章,正是本文架构分析的重点。
一句话定位 DuckDB:进程内(in-process)、嵌入式、列式向量化的 OLAP 数据库。它像 SQLite 一样零部署,却像数据仓库一样擅长分析。
二、核心概念:DuckDB 到底是什么
在写代码之前,先把几个决定它性格的核心概念讲清楚,否则后面的「为什么这么快」会显得玄学。
2.1 进程内 / 嵌入式,但不是 OLTP
DuckDB 没有独立的 server 进程。你 import duckdb 之后,引擎就跑在你的 Python 进程里;你 sql.Open("duckdb", "") 之后,它就跑在你的 Go 程序里。没有 socket、没有鉴权、没有连接池——这点和 SQLite 一致。
但二者面向的工作负载天差地别:
| 维度 | SQLite | DuckDB |
|---|---|---|
| 设计目标 | OLTP(事务、点查、增删改) | OLAP(扫描、聚合、分析) |
| 存储布局 | 行存为主 | 列式向量化 |
| 典型查询 | WHERE id = ? | SELECT region, SUM(x) FROM t GROUP BY region |
| 并发写 | 单写者 | 单写者(分析场景无所谓) |
| 擅长数据量 | MB ~ GB | GB ~ TB(单机能跑很大) |
所以不要拿 DuckDB 当业务主库,它也不适合高并发点查。它的主场是分析。
2.2 列式存储 + 向量化执行
这是 DuckDB 速度的真正来源,也是和 SQLite 最本质的区别。
传统行存数据库(以及 Pandas 的某些路径)一次处理一行。但分析查询往往是「对某一列求和 / 求均值 / 分组」。列式存储把同一列的数据物理上挨在一起,带来两个好处:
- 列裁剪(column pruning):查询只碰用到的列,其他列根本不读;
- 压缩友好:同一列数据类型相同,压缩率高,且压缩后的数据可以直接在压缩态上做部分运算。
但光列式还不够。「向量化执行」才是放大器。DuckDB 的执行不是一次处理一行、也不是一次处理一个值,而是一次处理一个 Vector(向量)——一个包含 2048 行的数据块(chunk)。运算符(filter、hash join、aggregation)都在「整块整块」的数据上批量操作。这意味着:
- 函数调用次数从「每行列一次」降到「每 2048 行列一次」,大幅减少解释开销;
- 现代 CPU 的 SIMD 指令可以对连续内存做并行计算;
- 分支预测、缓存局部性都被照顾到了。
你可以把它理解为:行存执行是「一条一条搬砖」,向量化执行是「一托盘一托盘搬砖」。
2.3 SQL-on-Files:文件即表
DuckDB 最反直觉、也最爽的能力是:你不需要先建表、导入数据。文件本身就是表。
-- 直接查一个 Parquet 文件,无需 CREATE TABLE
SELECT region, SUM(amount) AS total
FROM 'data/orders.parquet'
GROUP BY region
ORDER BY total DESC;
通配符、多文件、目录都支持:
-- 查一整个分区的多个文件
SELECT date, COUNT(*)
FROM 'data/orders/2026-*.parquet'
GROUP BY date;
它原生支持 Parquet、CSV、JSON、Arrow、Excel 等格式。对 CSV 也做了智能推断(read_csv_auto),对 Parquet 则能利用其中的统计信息做 谓词下推(predicate pushdown) 和 行组裁剪(row-group / zonemap pruning)——后面性能章节会展开。
2.4 零拷贝的 Arrow 桥梁
DuckDB 与 Apache Arrow 深度集成。Arrow 是一种跨语言的内存列式格式。DuckDB 查询的结果可以直接以 Arrow 格式「零拷贝」交给 Python 的 Pandas / Polars、R 的 data.frame 等,省去一遍序列化反序列化。对数据科学工作流,这是巨大的体验提升。
2.5 VARIANT:半结构化数据的「逃生舱」
DuckDB 1.x 引入了 VARIANT 类型——一个能容纳任意 JSON 式半结构化数据的动态类型。当你有一堆 schema 飘忽的 JSON 日志,又不想预先定义死所有字段时,把它丢进 VARIANT:
CREATE TABLE events (id BIGINT, payload VARIANT);
INSERT INTO events VALUES
(1, {'user': 'alice', 'score': 95, 'tags': ['a', 'b']}),
(2, {'user': 'bob', 'score': 88, 'meta': {'vip': true}});
-- 像访问对象属性一样访问嵌套字段
SELECT
payload.user,
payload.score,
payload.tags[1]
FROM events;
VARIANT 把「强类型数据仓库」和「灵活 JSON」之间的鸿沟填了一半,特别适合日志、埋点、事件流这类 schema 不稳定的数据。
2.6 扩展生态:一切皆可插
DuckDB 的内核保持精简,能力通过 扩展(Extension) 外挂。官方与社区提供了大量扩展:httpfs(读 S3/对象存储)、json、parquet、sqlite_scanner、postgres_scanner、mysql、iceberg、delta、lance、spatial、fts、excel、ducklake 等。扩展支持自动下载加载(autoload):
-- 第一次用到 S3 上的 Parquet 时,DuckDB 会自动 INSTALL + LOAD
SELECT * FROM read_parquet('s3://my-bucket/orders/*.parquet');
连扩展本身都能用 C# 写——2026-03 的 DuckDB.ExtensionKit 让 .NET 开发者也能造扩展。
三、架构分析:从一条 SQL 到结果,引擎里发生了什么
理解执行链路,才能理解后面 Async I/O 到底改了什么。
3.1 查询生命周期
一条 SQL 进入 DuckDB 后,经历:
SQL 文本
→ Parser(词法/语法分析,生成 AST)
→ Binder(语义绑定:表/列/类型解析,生成逻辑计划 Logical Plan)
→ Optimizer(基于规则 RBO + 基于代价 CBO 的等价变换)
→ Physical Planner(生成物理算子树 Physical Plan)
→ Executor(向量化执行,产出 Vector 流)
→ 结果(Arrow / DataFrame / 游标)
优化器做的事包括:谓词下推(把 WHERE 尽可能推到扫描阶段,让文件格式层就跳过不相关数据)、常量折叠、子查询去关联、join 重排、列裁剪等。这些对性能影响极大,后面会结合 Parquet 讲。
3.2 Morsel-Driven Parallelism:并行如何发生
DuckDB 的并行模型叫 Morsel-Driven Parallelism(基于「小片」的并行)。执行器把一个大任务切成很多 morsel(小数据片,约等于一个或几个 Vector),扔进一个任务队列;一组执行线程(默认等于物理核数)不断从队列里领 morsel 来处理。
这和传统的「一个算子一个线程、流水线式并行」不同——DuckDB 是「任务领取式」的:谁空闲谁领活,负载天然均衡,且能根据数据特征动态切分。你用 SET threads=4; 就能控制并行度。
3.3 向量化执行的内部:以 Hash Aggregation 为例
拿 SELECT region, SUM(amount) FROM t GROUP BY region 说。执行器不会逐行读 t:
- 扫描算子每次吐出一个 Vector(2048 行)的
region和amount; - Hash Aggregation 算子对这个 Vector 做:对每行算
hash(region),定位到哈希表中的分桶,把amount累加到对应桶的SUM累加器; - 所有 Vector 处理完后,哈希表里的每个桶就是一组
(region, sum); - 最后做最终的聚合/排序输出。
注意:整个累加过程是在「连续内存的整块数据」上循环的,CPU 缓存命中率高,还能触发编译器自动向量化(auto-vectorization)。这就是「向量化 + 列式」合起来的威力。
3.4 Async I/O 革命:Work, Thread, Work
这是 DuckDB 1.5.x 性能故事的核心,也是 2026-07-31 那篇深度文章的主题。我们得先搞清楚「老问题」。
旧模型的瓶颈。 分析查询常常被 I/O 卡住:数据要么在磁盘上、要么在 S3 这类对象存储里,而且 Parquet/CSV 读出来后是压缩的,必须解压才能交给执行器。在旧模型里,有一组专门的 I/O 线程负责「读取 + 解压」,把解压好的数据放到一个缓冲区;执行线程再从缓冲区取走 Vector 去算。问题在于:读取、解压、计算三者之间的「交接」存在同步点,执行线程在等下一批数据时常常空转;而解压这种 CPU 活本可以和前一批的计算真正并行起来,旧模型没把它充分利用。
新模型:Work, Thread, Work。 Pedro Holanda 描述的新思路,名字就点出了节奏:执行线程先 Work(处理当前这个已经就绪的 Vector),然后把这个 Vector 之后、下一批数据的 I/O + 解压 任务甩给一个后台线程(Thread) 异步去做,自己立刻去 Work(处理队列里下一个已经可用的 Vector)。于是:
- 执行线程几乎不空等——它永远在处理「就绪的活」;
- I/O 与解压被异步化,和「前一批的计算」在时间上重叠;
- 扫描密集、I/O 密集的查询(这是分析场景的绝对主流)延迟显著下降。
用一张简化时序来感受:
旧模型: [计算 V1] → 等 I/O/解压 → [计算 V2] → 等 I/O/解压 → ...
新模型: 线程A: [计算 V1]........[计算 V2]........[计算 V3]...
后台: [I/O+解压 V2][I/O+解压 V3][I/O+解压 V4]...
↑ 两者在时间轴上重叠,互不空等
一句话总结这次优化:它把「I/O + 解压」从执行线程的关键路径上拿掉,变成后台并行流水线。对于「读 Parquet 然后聚合」这类典型分析负载,这正是 1.5.x 在基准里把扫描吞吐推上去的原因。它不改变 SQL 语义,也不要求用户改任何代码——纯粹是引擎内部的执行模型升级。
3.5 内存之外:Out-of-Core 与溢出
「单机能跑多大?」——DuckDB 支持 Out-of-Core 执行:当内存不够时,它会把中间结果(如 hash 表、排序缓冲区)溢出(spill)到磁盘,而不是直接 OOM。配合 SET memory_limit='4GB';,你能让它在一台 16G 内存的机器上稳稳处理几十 GB 甚至上 TB 的文件。
-- 限制内存上限,超出部分自动落盘溢出
SET memory_limit = '4GB';
SET threads = 4;
-- 之后随便跑大查询
SELECT user_id, COUNT(*) FROM huge_events GROUP BY user_id;
这也是 DuckDB 敢说「大于内存也能处理」的底气。对比 Pandas:df = pd.read_csv('8g.csv') 在 16G 机器上基本必死;而 DuckDB 的流式扫描 + 溢出机制,让它能在有限内存下把活干完。
四、代码实战:多种语言把 DuckDB 用起来
光讲原理不过瘾,下面直接上手。覆盖 Python、纯 SQL/CLI、Go、Rust 四条路径。
4.1 Python:最顺手的入口
安装:
pip install duckdb
场景 A:直接查文件,零导入。
import duckdb
# 直接对一个 Parquet 做即席分析,连表都不用建
rel = duckdb.sql("""
SELECT region,
COUNT(*) AS orders,
SUM(amount) AS revenue,
AVG(amount) AS avg_amount
FROM 'data/orders.parquet'
WHERE amount > 100
GROUP BY region
ORDER BY revenue DESC
""")
# 转成 pandas / polars 都行(零拷贝走 Arrow)
df = rel.df() # pandas DataFrame
print(df.head())
场景 B:和 Pandas 互操作(DataFrame 即表)。
import duckdb
import pandas as pd
df = pd.read_csv('events.csv') # 哪怕 df 很大,DuckDB 也能在上面做分析
result = duckdb.query("""
SELECT event_type,
COUNT(*) AS cnt
FROM df -- 直接引用 Python 变量名当表用
GROUP BY event_type
ORDER BY cnt DESC
""").df()
print(result)
场景 C:大于内存的 CSV → Parquet 转换(经典 ETL)。
import duckdb
# 一次性把大 CSV 转成列式 Parquet,之后查询 Parquet 快得多
duckdb.sql("""
COPY (
SELECT * FROM read_csv_auto('data/big_log.csv', sample_size=100000)
) TO 'data/big_log.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)
""")
# 之后查询 Parquet:列裁剪 + 谓词下推,秒级响应
rows = duckdb.sql("""
SELECT date, COUNT(*) FROM 'data/big_log.parquet'
WHERE level = 'ERROR'
GROUP BY date
""").fetchall()
print(rows)
场景 D:直接读 S3 上的 Parquet(需要 httpfs 扩展)。
import duckdb
duckdb.sql("INSTALL httpfs; LOAD httpfs;")
# 用环境变量或 SET 配置凭证(生产请用凭证链,勿硬编码)
duckdb.sql("""
SET s3_region = 'us-east-1';
SET s3_access_key_id = '<YOUR_KEY>';
SET s3_secret_access_key = '<YOUR_SECRET>';
""")
rel = duckdb.sql("""
SELECT region, SUM(amount)
FROM 's3://my-bucket/orders/2026-*.parquet'
GROUP BY region
""")
print(rel.df().head())
安全提示:
s3_secret_access_key这类密钥务必来自环境变量或密钥管理,不要写死在源码里。
4.2 纯 SQL / CLI:运维与探索的神器
DuckDB 自带命令行工具,安装后用 duckdb 即可进入交互式,也支持一行命令:
# 交互式
duckdb analytics.db
# 一行式即席查询(非常适合 shell 管道 / 定时任务)
duckdb -c "SELECT region, sum(amount) FROM 'orders.parquet' GROUP BY region"
# 从文件读 SQL
duckdb -c ".read analysis.sql"
# 把查询结果直接导出为另一个 Parquet
duckdb -c "COPY (SELECT * FROM 'orders.parquet' WHERE amount>100)
TO 'filtered.parquet' (FORMAT PARQUET)"
CLI 还支持 .mode、.timer、.explain 等,做探索性分析非常顺手。
4.3 Go:把分析能力嵌进后端服务
Go 生态里常用 github.com/marcboeker/go-duckdb(database/sql 风格驱动)。
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/marcboeker/go-duckdb/v2"
)
func main() {
// 第二个参数是数据库文件路径,传空字符串表示内存库
db, err := sql.Open("duckdb", "")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 直接在文件上做分析,无需建表导入
var total int
err = db.QueryRow(
"SELECT COUNT(*) FROM read_parquet('data/orders.parquet') WHERE amount > 100",
).Scan(&total)
if err != nil {
log.Fatal(err)
}
fmt.Printf("符合条件的订单数: %d\n", total)
// 多行结果遍历
rows, err := db.Query(
"SELECT region, SUM(amount) AS revenue FROM 'data/orders.parquet' GROUP BY region ORDER BY revenue DESC",
)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var region string
var revenue float64
if err := rows.Scan(®ion, &revenue); err != nil {
log.Fatal(err)
}
fmt.Printf("%-10s %.2f\n", region, revenue)
}
}
在 Go 后端里,你可以用它做「请求进来 → 对本地落盘的 Parquet/CSV 做即席聚合 → 返回 JSON」这类轻量分析 API,全程不必引入外部数据仓库。
4.4 Rust:类型安全地嵌分析引擎
Rust 侧用官方 duckdb crate:
use duckdb::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(
"CREATE TABLE users(id INTEGER, name VARCHAR);
INSERT INTO users VALUES (1, 'alice'), (2, 'bob'), (3, 'carol');",
)?;
// 参数化查询
let mut stmt = conn.prepare("SELECT count(*) FROM users WHERE id > ?")?;
let mut rows = stmt.query([1])?; // 过滤 id > 1
while let Some(row) = rows.next()? {
let c: i64 = row.get(0)?;
println!("count = {}", c); // 2
}
// 直接分析外部文件
let mut stmt2 = conn.prepare(
"SELECT region, SUM(amount) FROM read_parquet('data/orders.parquet') GROUP BY region",
)?;
let mut rows2 = stmt2.query([])?;
while let Some(row) = rows2.next()? {
let region: String = row.get(0)?;
let sum: f64 = row.get(1)?;
println!("{}: {:.2}", region, sum);
}
Ok(())
}
4.5 DuckLake:用 SQL 写就的湖仓格式
2026-04 DuckDB 推出 DuckLake v1.0——一种「建在 SQL 之上的湖仓格式」。它把元数据用 DuckDB 自己存,数据以 Parquet 落在对象存储,兼顾事务、时间旅行与低成本。做个最小示例:
INSTALL ducklake; LOAD ducklake;
-- 元数据可放本地/对象存储,数据放在对象存储的 DATA_PATH
ATTACH 'ducklake:metadata.db' AS mylake (DATA_PATH 's3://my-bucket/lake-data/');
CREATE SCHEMA mylake.main;
USE mylake.main;
CREATE TABLE sales (id INTEGER, amount DOUBLE, region VARCHAR);
INSERT INTO sales VALUES (1, 99.5, 'east'), (2, 120.0, 'west');
-- 时间旅行:看到某个版本之前的数据
SELECT * FROM sales;
-- 通过 DuckLake 的版本/快照机制回看历史
DuckLake 的意义在于:它把「湖仓」这件过去要搭 Iceberg/Delta + 一整套 catalog 的事,收敛到 DuckDB 一个引擎里,进一步坐实「本地优先数据栈」的趋势。
五、性能优化:15 条生产级实战建议
DuckDB 开箱快,但用错姿势照样慢。下面 15 条来自真实踩坑。
1. 优先 Parquet,而非 CSV。 Parquet 是列式、自带统计信息(min/max per row-group),DuckDB 能据此做谓词下推和行组裁剪;CSV 是行式文本,几乎只能全扫。一次性 COPY ... TO 'x.parquet' 转换,后续查询天差地别。
2. 用分区目录 + 通配符,而不是一个大文件。 按日期/地区分成 2026-08-01.parquet、2026-08-02.parquet……查询用 'data/2026-08-*.parquet',DuckDB 只打开匹配的文件,配合文件名里的分区信息甚至能跳过整目录。
3. 把过滤条件写在 WHERE 里,让它下推。 WHERE amount > 100 AND date = '2026-08-01' 会被尽量推到扫描阶段,Parquet 直接用 row-group 的 min/max 跳过无关数据块。
4. 只 SELECT 需要的列。 列裁剪意味着少读几列就少读几块数据。别无脑 SELECT *,尤其宽表。
5. 合理设置 memory_limit。 太小会频繁溢出拖慢;太大在共享机器上会挤压别的进程。根据机器内存给个明确值:SET memory_limit='4GB';。
6. 控制 threads。 默认等于物理核数。在容器/虚拟机里若核数被限制,手动 SET threads=4; 避免创建过多线程空耗。
7. 用 read_csv_auto 的 sample_size 提升类型推断准确率。 大 CSV 默认抽样推断类型,若首行样本有偏可加大 sample_size。
8. 重复查询用 Prepared Statement。 参数化查询(? / $1)能复用执行计划,避免重复解析绑定。
9. 中间结果物化,避免重复扫描。 同一份数据被多个查询反复用?CREATE TABLE stage AS SELECT ... 或 INSERT INTO 物化一次,后续直接查表。
10. 增量写入用 APPEND。 持续导入时 INSERT INTO target SELECT * FROM read_parquet('new_*.parquet') 追加,而不是反复重建。
11. 用窗口函数替代自连接。 ROW_NUMBER() OVER (PARTITION BY user ORDER BY ts) 比「自己 join 自己」快得多,也更易读。
12. 容忍近似时用近似聚合。 approx_count_distinct(col)、quantile_cont 的近似版本在大数据集上显著更快,看趋势足够。
13. 用 EXPLAIN / EXPLAIN ANALYZE 看执行计划。 怀疑慢就 EXPLAIN ANALYZE SELECT ...,看哪个算子耗时、是否真的下推了谓词、是否发生了意外溢出。
14. 压缩选 ZSTD。 转 Parquet 时用 COMPRESSION ZSTD,压缩率高、解压快,整体 I/O 反而更少。
15. 大表 JOIN 注意连接键类型一致。 一个 INT 一个 BIGINT 会触发隐式转换、破坏哈希效率,甚至改变并行度。JOIN 前确保两端类型对齐。
附一个性能对比直觉(非精确基准,仅说明量级):在「单文件 10GB CSV 上做 GROUP BY 聚合」的场景,Pandas 往往因内存爆炸失败或极慢,而 DuckDB 列式扫描 + 溢出能在有限内存下完成;相比起一套为这点数据而部署的 ClickHouse,DuckDB 省掉的运维成本更是数量级优势。它不追求比分布式引擎更快,而是追求「在这个量级下,零部署就能快」。
六、总结展望:DuckDB 在 2026 数据栈里的位置
把视野拉回全局,DuckDB 不是要取代谁,而是重新定义了「分析的起点」。
它填补的是「SQLite 到 Snowflake 之间」那片长期被忽视的地带。 过去你要么忍受 SQLite 不会分析,要么为了一次聚合去够一个庞大的数据仓库。DuckDB 用「嵌入式 + 向量化 + SQL-on-Files」三件套,把这个中间地带抹平了:数据分析师、后端工程师、数据工程师、科研人员,都能在各自的进程里、各自的文件上,直接拿到数据仓库级别的分析能力。
几个正在发生的趋势值得关注:
- 本地优先(Local-first)数据栈。 DuckDB + 本地 Parquet 文件,构成一个「不依赖中心服务」的分析单元。结合 Arrow,Pandas/Polars/ DuckDB 之间流转几乎零成本。
- 湖仓平民化:DuckLake。 2026-04 的 DuckLake v1.0 把湖仓的复杂度收敛进一个引擎,元数据用 SQL 管、数据用 Parquet 存,事务与时间旅行开箱即用。
- 云边协同:MotherDuck 与 Quack 协议。 官方推出的 Quack 客户端-服务端协议(2026-05),让本地 DuckDB 与云端(如 MotherDuck)无缝协同——小的在本地算,大的丢给云,一套 SQL 语言。
- 格式全家桶。 Iceberg、Lance、Delta(+Unity Catalog、Time Travel)、Vortex 陆续接入,DuckDB 正成为「开放式数据格式的万能查询入口」。
- 执行模型持续进化。 从 1.5.0 到 1.5.5,Async I/O「Work, Thread, Work」把扫描密集查询的 I/O 与计算真正重叠,这正是嵌入式引擎在有限资源下榨干硬件的典型思路——未来在异步、向量化、溢出之间还会有更多文章。
但也别忘了它的边界:
- 不是业务主库,不支持高并发写、没有多写者;
- 不适合高 QPS 的在线点查(那是 SQLite / PostgreSQL 的活);
- 跨机器共享、强一致多租户,还是要上真正的分布式/云数据仓库。
一句话收尾: DuckDB 的聪明之处,不在于它发明了多少新东西,而在于它把「列式存储、向量化执行、进程内嵌入、SQL-on-Files、Arrow 零拷贝」这些成熟理念,严丝合缝地拼成了「数据分析的最小可用单元」。当你下次面对一个「不大不小」的数据文件、不想为它折腾一整套基础设施时,记住:先 import duckdb,可能这一行就够你跑完今天所有的分析。
本文参考 DuckDB 官方工程博客(DuckDB 1.5.5 发布说明、2026-07-31 Pedro Holanda《Asynchronous I/O in DuckDB: Work, Thread, Work》、DuckLake v1.0 公告、Quack 协议公告)。代码示例以 DuckDB 1.5.x 语法为准,生产环境请结合实际版本与凭证管理规范使用。