DuckDB v2.0 新特性前瞻
本文提要:DuckDB v2.0将于今年秋季发布。本文将提前介绍其重磅功能:服务端模式、触发器、VARIANT类型、异步I/O、全新SQL解析器、全新存储格式等。
DuckDB v2.0代号为“Cyanoptera”,得名于桂红鸭(Anas cyanoptera)——一种羽毛呈亮眼红棕色的鸭类,栖息于美洲西部。
我们不会轻易启动大版本迭代,这绝非形式主义:v2.0搭载了全新SQL解析器、默认存储格式、重构后的C API,以及少量经过审慎考量的破坏性变更。但核心而言,这是一次功能爆发式更新——自今年3月v1.5发布以来,我们已累积提交超10000次代码。去年是湖仓元年,而本次版本则开启了DuckDB的服务端时代。若你更偏好视频形式,可观看DuckCon #7大会上的“鸭态报告”演讲,其中也预览了诸多新功能。
DuckDB的迭代速度极快,本文仅能覆盖一小部分更新。筛选核心功能的过程如同一场“取舍之战”——没错,接下来的内容本质上是一篇“十大看点”式清单(《DuckDB v2.0十大新功能,第八个惊掉你下巴》)。我们虽对这种形式不甚满意,但它确实有效,那就言归正传:先从SQL层面的功能说起,再逐步深入引擎底层。
一、服务端模式:响应呼声,正式上线
DuckDB从诞生起就是一款进程内数据库,但用户一直强烈要求我们推出客户端/服务端模式,如今我们终于响应了这一需求。Quack扩展实现了DuckDB的原生通信协议,支持不同DuckDB实例间交互。该扩展在DuckCon #7前夕以预览版形式发布,将在v2.0中正式转为稳定版,也是DuckDB未来发展的核心方向之一:任意DuckDB进程都可通过网络对外提供数据库服务,其他DuckDB实例则可通过新增的CONNECT语句连接并发送查询。
例如:
CONNECT替代了Quack首次亮相时我们展示的remote.query($$...$$)临时方案——我们认为那种语法不够理想。而且CONNECT并非仅支持Quack:它可将会话指向任何兼容的远程数据库,新增的远程下推优化器(#22914)能直接将SQL发送至PostgreSQL和MySQL执行,无需先拉取整张表:
CONNECT 'postgres://localhost/mydb';
SELECT count(*) FROM orders; -- 该查询将在PostgreSQL服务器上执行
DISCONNECT;
你或许会认为DuckDB无法处理事务型工作负载,但实际上,DuckDB从一开始就具备完整的MVCC(多版本并发控制)和事务隔离机制,是一款支持多连接的事务型数据库。只是多数单用户场景下无需用到这些能力。实践证明,DuckDB的事务处理性能优异:在不少工作负载下,其速度足以与PostgreSQL等通用数据库媲美,而客户端/服务端模式终于让这套机制在多租户、长期运行的部署环境中大放异彩。
长期运行DuckDB也带来了新挑战,因此v2.0着重强化了指标、日志和可观测性(例如#22799中重构的指标层),让你能清晰掌握DuckDB实例的运行状态。Quack预览版发布仅数周,就有人为其协议开发了独立客户端。我们原本只是想让DuckDB能与其他DuckDB通信,结果社区直接开发了自己的客户端,这着实出乎意料。
二、VARIANT类型:JSON的“性能增强版”
VARIANT类型已在DuckDB v1.5中推出,可以将其理解为“性能拉满的JSON”。想象一下,如果JSON能跑得飞快会是什么样子——VARIANT就是答案。和JSON一样,VARIANT列的每行可存储不同结构的数据;但它并非文本格式:DuckDB会自动检测半结构化数据中隐藏的通用结构并“拆解”,使其在存储时具备高压缩率,查询时执行速度极快,全程无需你手动定义 schema。这让VARIANT成为实时日志摄入的理想选择——这类场景中,类JSON记录流虽共享部分结构,但会随时间演变。
v2.0实现了VARIANT端到端的高效处理:从存储直接读取拆解后的数据执行查询(#20912)、将提取操作下推至扫描阶段(#22478)、支持Parquet格式的拆解式VARIANT读写,以及一系列variant_*函数:
CREATE TABLE events (payload VARIANT);
INSERT INTO events
VALUES ('{"user": {"id": 42, "tags": ["a", "b"]}}'::JSON::VARIANT);
SELECT variant_type(payload), variant_keys(payload)
FROM events;
SELECT *
FROM events
WHERE variant_contains(payload, {'user': {'id': 42}}::VARIANT);
长期来看,我们计划在v2.0发布后不久(但不做承诺),让常规JSON类型底层基于VARIANT实现,这样现有JSON工作负载无需修改任何查询,就能自动获得上述所有性能优势。
三、触发器:完整支持,灵活扩展
触发器是用户长期以来的高频需求,DuckDB v2.0将全面支持:包括BEFORE和AFTER触发器、FOR EACH ROW和FOR EACH STATEMENT触发方式、通过REFERENCING OLD/NEW TABLE访问过渡表、同一事件可绑定多个触发器、触发表支持RETURNING子句,以及DROP TRIGGER语句。
经典应用场景是审计表:当系统发生变更时,触发器自动记录修改内容。例如:
CREATE TABLE target (id INTEGER, val INTEGER);
CREATE TABLE audit (id INTEGER, old_val INTEGER, new_val INTEGER);
CREATE TRIGGER trg_audit AFTER UPDATE ON target
REFERENCING OLD TABLE AS o NEW TABLE AS n
FOR EACH STATEMENT
INSERT INTO audit
SELECT n.id, o.val, n.val
FROM o
JOIN n ON o.id = n.id;
INSERT INTO target VALUES (1, 10), (2, 20);
UPDATE target SET val = val * 10 WHERE id <= 2;
SELECT * FROM audit;
| id | old_val | new_val |
|---|---|---|
| 1 | 10 | 100 |
| 2 | 20 | 200 |
触发器与长期运行的DuckDB服务天然适配,我们也计划在内部用它构建多个后续功能。同时,触发器完全在SQL层面暴露,你可以用它打造自定义的功能。
四、SQL语法扩展:功能更丰富,写法更简洁
DuckDB的SQL方言持续扩充,以下是本次版本的几项亮点:
- NEAREST连接(#24137):将Top-K相似度搜索整合为连接子句,非常适合向量和嵌入向量工作负载:
SELECT q.user_id, t.product_id FROM users q INNER JOIN products t APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(q.embedding, t.embedding); - CTE中支持DML(#21634、#21997、#24217):允许将
INSERT、UPDATE、DELETE和COPY作为数据处理流水线的步骤:WITH moved AS MATERIALIZED ( DELETE FROM staging RETURNING * ) INSERT INTO archive SELECT * FROM moved; - 嵌套 schema(#23492、#24222):支持在schema内部再创建schema:
CREATE SCHEMA finance; CREATE SCHEMA finance.reports; CREATE TABLE finance.reports.q3 (revenue DECIMAL); - 新变量语法(#21194):可在任何表达式中使用
$x形式的变量,无需再写繁琐的getvariable(...):SET VARIABLE threshold = 100; SELECT * FROM orders WHERE amount > $threshold; - JSON修改函数:新增
json_set、json_insert、json_replace和json_remove(#23786),终于支持直接修改JSON文档:SELECT json_set('{"a":1}', '$.b', '2');json_set('{"a":1}', '$.b', '2') {"a":1,"b":2} - 带
USING KEY聚合的递归CTE(#19481):依托重构后的递归CTE引擎,可在纯SQL中实现迭代算法:WITH RECURSIVE tbl(a, b) USING KEY (a, avg(b)) AS ( SELECT 1, 5 UNION SELECT a, b - 1 FROM tbl WHERE b > 0 ) TABLE tbl;a b 1 2.5
除此之外,还有SQL标准的FETCH FIRST 2 ROWS ONLY(#23533)、OVERLAY()函数(#22456)、GROUP BY中支持UNNEST(#23644),以及针对多行匹配场景的MERGE/UPDATE ... FROM明确定义语义(#24058)。
五、异步I/O:大幅提升对象存储访问速度
与S3等对象存储交互是DuckDB的核心使用场景之一——数据总得有来源,而很多数据就存储在对象存储中。DuckDB早已支持并行读取对象存储,但同步访问模式限制了速度上限。v2.0在整个引擎中引入了异步I/O,我们已在专门的博客文章中详细介绍了设计思路。
借助异步访问,I/O层可独立于查询处理层扩展,这意味着远程读取的并行度大幅提升,网络存储上的查询速度将显著加快。目前已率先支持Parquet格式(#23662),CSV(#23961)和DuckDB自有格式(#24654)将紧随其后,同时还支持异步Parquet写入(#23283)以及新增的MMAP和DIRECT_IO模式(#22988)。本地存储也能获得小幅性能提升,但网络存储的改善最为明显。
六、性能优化:现有查询自动提速
和以往版本一样,我们投入大量精力让你的现有查询无需任何修改就能跑得更快。以下是部分亮点:
- 部分聚合操作可下推至连接操作之前(#22572),且可复用冗余聚合计算(#24543)
- 重构了递归CTE引擎(#22211)
- 聚合操作在内存不足时可自动溢出到磁盘(#24499)
- Windows CLI的多线程结果物化速度提升约2.2倍(#24036)
性能提升有多显著?你可以在笔记本上运行这个微基准测试:用普通递归CTE实现含100万条边的图的单源可达性分析。
CREATE TABLE edges AS
SELECT (range % 100_000)::INTEGER AS src,
((range * 13 + 7) % 100_000)::INTEGER AS dst
FROM range(1_000_000);
WITH RECURSIVE reachable(node) AS (
SELECT 0
UNION
SELECT dst FROM edges, reachable WHERE src = node
)
SELECT count(*) FROM reachable;
| 版本 | 运行时间 |
|---|---|
| DuckDB v1.5.4 | 4.90秒 |
| DuckDB v2.0(预览版) | 0.12秒 |
可见,针对同一递归查询,DuckDB v2.0的速度提升了约40倍!
七、数据裁剪与分区感知:减少不必要扫描
行组裁剪能力大幅扩展:[最小-最大索引(区域映射)](https://duckdb.org/docs/current/sql/indexes.html#min-max-index-zonemap %})和Parquet布隆过滤器现在支持对结构体、列表、小数、UUID、IN过滤器甚至函数谓词进行数据裁剪:
-- 以下查询现在会跳过无关行组,而非全表扫描:
SELECT * FROM logs WHERE contains(message, 'ERROR');
SELECT * FROM t WHERE substr(code, 1, 3) = 'NL-';
SELECT * FROM 'data/*.parquet' WHERE id IN (1, 5, 9);
查询规划也具备了分区感知能力(#22336)。湖仓格式(DuckLake、Iceberg以及S3上的普通Hive分区Parquet)均采用分区存储,能否利用分区往往决定了是扫描全量数据还是跳过大部分数据。v2.0中,查询规划器和优化器可充分利用现有分区结构,同时分区写入功能也已重构(#22225、#22620)。
八、全新存储格式:大索引宽表启动更快、内存占用更低
DuckDB v2.0将默认存储格式版本升级至v2.0.0(#22875)。核心改进是基于缓冲区管理的ART索引(#21458、#23605):索引不再常驻内存,这意味着带大索引的表可瞬间打开,索引数据仅在需要时才会分页加载。
列元数据现在采用延迟加载(#22333),宽表的打开速度也更快。默认启用DICT_FSST字符串压缩算法(#23733),删除操作的存储更紧凑(#24336),存储层在读取时会执行更严格的损坏校验。简言之:带大索引的宽表打开速度更快,内存占用大幅降低。
九、全新SQL解析器:可扩展、更友好
DuckDB一直使用基于PostgreSQL的解析器,但如今我们决定彻底替换:v2.0搭载了自研的现代化、可扩展PEG解析器(#22194),这一想法最早源于我们2024年发布的关于运行时可扩展解析器的文章。这一变更与扩展生态系统深度绑定:扩展现在可直接对接语法规则,未来有望出现能扩展全新SQL语法的扩展。同时,新解析器还能提供更友好的错误信息,附带精确的源代码位置,并且首次支持方言兼容模式:
SET dialect_compatibility_mode = 'spark';
我们设计新解析器时确保了与旧版本的兼容性,正常使用时你不会感知到变化。如果发现兼容性问题,欢迎提交Issue。
十、移除ICU依赖:更小、更快、更易维护
DuckDB的时区感知时间戳、日历和排序规则一直由ICU库提供支持。ICU是个不错的库,但我们仅用到了其一小部分功能,却要在每个DuckDB发行版中携带整个库。v2.0中,ICU库已被完全移除:icu扩展现在自行实现时区、日历和排序规则(#24463、#24403),时区数据直接基于IANA数据库构建并压缩至约45KB。所有功能保持与之前完全一致:
SELECT '2026-08-14 12:00:00'::TIMESTAMPTZ AT TIME ZONE 'Europe/Paris';
SELECT * FROM names ORDER BY name COLLATE de;
除了体积更小、更易更新,新实现的性能也更出色。以下是在MacBook上的微基准测试结果:转换2500万条时间戳的时区,以及用德语排序规则过滤500万条字符串:
| 查询 | v1.5.4(ICU) | v2.0(原生实现) | 性能提升 |
|---|---|---|---|
ts AT TIME ZONE 'Europe/Paris',2500万行 |
0.24秒 | 0.11秒 | 2.2倍 |
带COLLATE de的过滤,500万行 |
0.15秒 | 0.06秒 | 2.6倍 |
扩展生态:稳定C API与自定义仓库
扩展是DuckDB的一大优势,但目前大多数扩展(包括官方扩展)都基于不稳定的C++ API开发。这意味着扩展开发者需要针对每个DuckDB版本重新适配和构建,社区扩展也可能因作者停止维护而悄然消失。DuckDB v2.0大幅扩充了稳定C API,让扩展只需开发、构建、发布一次,就能长期兼容后续版本。
为确保长期可持续性,C API现在由一份声明式的版本化规范自动生成(#24135):duckdb.h、duckdb_extension.h中的所有函数以及扩展ABI都在api_spec/目录的YAML文件中定义,完整生命周期可追溯,CI会验证提交的头文件是否符合规范,确保API和ABI不会出现偏差。本次版本还带来了统一的符号版本控制(#24435)、自定义分配器(#23945),以及支持将C API扩展静态链接到应用程序(#22251)。
基于稳定C API开发扩展是什么体验?以下是一个完整的扩展示例:单个文件注册一个向量化标量函数,只需编译一次即可对接duckdb_extension.h:
#include "duckdb_extension.h"
DUCKDB_EXTENSION_EXTERN
// 一个标量函数,逐向量计算两个BIGINT的和
static void AddNumbers(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) {
idx_t count = duckdb_data_chunk_get_size(input);
int64_t *a = (int64_t *) duckdb_vector_get_data(duckdb_data_chunk_get_vector(input, 0));
int64_t *b = (int64_t *) duckdb_vector_get_data(duckdb_data_chunk_get_vector(input, 1));
int64_t *result = (int64_t *) duckdb_vector_get_data(output);
for (idx_t row = 0; row < count; row++) {
result[row] = a[row] + b[row];
}
}
DUCKDB_EXTENSION_ENTRYPOINT(duckdb_connection con,
duckdb_extension_info info,
duckdb_extension_access *access) {
duckdb_scalar_function f = duckdb_create_scalar_function();
duckdb_scalar_function_set_name(f, "add_numbers");
duckdb_logical_type bigint = duckdb_create_logical_type(DUCKDB_TYPE_BIGINT);
duckdb_scalar_function_add_parameter(f, bigint);
duckdb_scalar_function_add_parameter(f, bigint);
duckdb_scalar_function_set_return_type(f, bigint);
duckdb_destroy_logical_type(&bigint);
duckdb_scalar_function_set_function(f, AddNumbers);
duckdb_register_scalar_function(con, f);
duckdb_destroy_scalar_function(&f);
return true;
}
LOAD add_numbers;
SELECT add_numbers(40, 2);
为简洁起见,我们省略了NULL值处理。完整版本可参考demo_capi扩展。
编译生成的二进制文件可跨DuckDB版本使用,无需每次新版本发布都重新适配或构建。如今借助AI工具,开发扩展从未如此简单。
自定义扩展仓库:自主托管,安全可控
开发完扩展后,该如何分发?此前DuckDB仅能从内置仓库(core、core_nightly、community等)安装扩展。v2.0中,你将可以注册自己的可信仓库(#24777,目前仍在开发中),企业可自行托管并签名扩展,安装和加载方式与内置扩展完全一致:
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY my_repo FROM 'https://extensions.example.org';
INSTALL my_ext FROM my_repo;
LOAD my_repo/my_ext;
仓库包含名称、URL前缀以及一个或多个用于验证扩展签名的RSA公钥。前缀可以指向任何DuckDB能读取的位置:本地路径、https、s3等。执行CREATE时,DuckDB会获取仓库的公钥并固定到仓库定义中,同时打印每个密钥的SHA-256指纹,方便你与线下发布的指纹比对。若你完全不信任网络,也可直接传入密钥:
CREATE EXTENSION REPOSITORY my_repo FROM 's3://my-bucket/extensions'
USING PUBLIC KEY '-----BEGIN PUBLIC KEY----- ...';
固定的仓库会在重启后保留,支持通过信任多个密钥实现密钥轮换,可随时通过duckdb_extension_repositories()表函数审计,也可通过DROP EXTENSION REPOSITORY删除。结合稳定C API,扩展生态形成了完整闭环:一次开发、签名后,可托管在任意位置,并在任何环境中通过INSTALL安装。
社区治理:新增利益相关方咨询委员会
今年秋季起,DuckDB基金会将新增一个利益相关方咨询委员会。该委员会将为DuckDB、DuckLake和Quack的开发路线图提供输入,让核心利益相关方能够参与项目方向的决策。
以上仅为部分亮点,本文也只是一份预览。今年秋季正式发布前,部分细节可能仍会调整,还有更多功能和改进未能在此一一介绍。DuckDB v2.0还将包含少量破坏性变更,包括新的默认存储格式和完成lambda语法过渡,这些内容将在正式发布公告中详细说明。
自v1.5发布以来,已有众多贡献者提交了超10000次代码。我们衷心感谢社区提供的详细Issue报告、反馈和代码贡献,正是这些内容塑造了本次版本。若你想提前体验,预览版构建已包含大部分新功能,若遇到问题,欢迎前往Issue追踪器反馈。