← 文章 / 数据与数据库
Hacker News 22小时前 · 2026-08-18 20:40:46 · 1 阅读

DuckDB v2.0 新特性前瞻

一窥 DuckDB v2.0

Mark Raasveldt 和 Hannes Mühleisen 2026-08-17 | 16 分钟

太长不看:DuckDB v2.0 将于今年秋季发布。本文将带您预览它的重磅特性:把 DuckDB 作为服务器、触发器、VARIANT 类型、异步 I/O、全新 SQL 解析器、全新存储格式,等等。

DuckDB v2.0 将被命名为 "Cyanoptera",得名于桂红鸭(Anas cyanoptera)——一种分布于美洲西部、羽色呈醒目的红棕色的鸭。

主版本号的提升从来不是轻易之举,也绝非走过场:v2.0 带来了全新的 SQL 解析器、全新的默认存储格式、重构的 C API,以及少量经过审慎考量的破坏性变更。但归根结底,这是一次特性发布——自今年三月 v1.5 释出以来,凝聚了一万余次提交。去年的关键词是 lakehouse,而此次发布则拉开了"DuckDB 作为服务器"这一年的序幕。多数特性我们在 DuckCon #7 的 "State of the Duck" 演讲中已作过演示,偏好视频形式的朋友可以前往观看。

DuckDB 演进速度很快,本文只能覆盖其中很小一部分。把所有新特性浓缩成一份短名单,总免不了一番取舍——没错,我们清楚接下来的内容本质上就是一篇盘点帖("DuckDB v2.0 即将到来的十件事,第八件令人震惊")。我们对这种形式并无偏爱,但它确实管用,所以就有了这篇文章。我们先从 SQL 层面的特性说起,再逐步深入到引擎内部。

1. 把 DuckDB 作为服务器:Quack 与 CONNECT

DuckDB 自诞生之初就是一款进程内数据库。但用户——非常执着地——向我们提出了客户端/服务器模式的需求,我们终于松口了。quack 扩展实现了 DuckDB 用来与其他 DuckDB 实例通信的原生协议。该协议在 DuckCon #7 前不久作为预览版发布,并将在 v2.0 中升级为稳定版。它是 DuckDB 未来走向的重要一环:任何 DuckDB 进程都可以通过网络对外提供数据库服务,而其他任何 DuckDB 实例都可以通过全新的 CONNECT 语句接入并路由查询。例如:

DuckDB 服务器

CALL quack_serve(
    token = 'my_token'
);
quack:

DuckDB 客户端

ATTACH 'quack:server.example.com'
    AS qk (TOKEN 'my_token');

CONNECT qk;
SELECT count(*) FROM events;
-- 在服务器端执行,
-- 结果流式返回
DISCONNECT;

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 以实现跨库通信;结果世界说“不不不”,并自己构建了客户端。谁能想到呢。

2. VARIANT 成为了一等公民

VARIANT 类型随 DuckDB v1.5 一同发布,可以把它理解为 JSON 的增强版。简单来说,就是假设 JSON 也能跑得飞快。和 JSON 一样,VARIANT 列允许每行存储不同结构的数据;但与 JSON 不同的是,它并非文本格式:DuckDB 会自动识别半结构化数据中隐藏的公共结构,并对其进行"shredding"(拆解),从而实现高压缩存储和快速查询,而且全程无需手动定义 schema。这让 VARIANT 天然适合实时日志摄入场景——JSON 风格的数据流既有共同结构,又会随时间演变。

在 v2.0 中,这条流水线已实现端到端打通:从存储直接执行 shredded 执行(#20912)、将字段提取下推到扫描阶段(#22478)、Parquet 的 shredded 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 工作负载无需修改任何查询就能享受到上述所有性能提升。

3. 触发器(Triggers)

触发器一直是大家长期呼吁的功能,v2.0 完整地实现了它:支持 BEFOREAFTER 触发器、FOR EACH ROWFOR EACH STATEMENT、通过 REFERENCING OLD/NEW TABLE 使用过渡表、同一事件可挂多个触发器、触发器表上的 RETURNING 子句,以及 DROP TRIGGER

最经典的用途是审计表:系统中发生某个操作时,触发器自动记录变更内容。例如:

触发器天然契合长时间运行的 DuckDB 服务,我们还计划在内部用它来构建若干新功能。它们在 SQL 层面也完全开放,你可以基于它们搭建各种实用的东西。

4. SQL 方言新增

一如既往,DuckDB 的 SQL 方言在持续扩充。本发布周期中几个值得关注的特性: 通过 **NEAREST 连接**(#24137),top-k 相似度搜索可以直接写成 join 子句,特别适合向量和 embedding 类工作负载:
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)让你可以把 INSERTUPDATEDELETECOPY 直接作为流水线步骤使用:
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_setjson_insertjson_replacejson_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)。

5. 异步 I/O

与 S3 这类对象存储交互是 DuckDB 体验中至关重要的一环:数据总要有出处,而它们往往就躺在对象存储里。DuckDB 早就能并行读取对象存储,但同步访问限制了速度上限。DuckDB v2.0 在整个引擎中引入了异步 I/O。我们在一篇专门的博客文章中详细介绍了其设计。

得益于异步访问,I/O 层现在可以独立于查询处理层进行扩展,这意味着远程读取的并行度大幅提升,网络存储上的查询速度也显著加快。Parquet 格式率先支持(#23662),随后是 CSV(#23961)和 DuckDB 自己的文件格式(#24654),同时还加入了异步 Parquet 写入(#23283)以及新的 MMAPDIRECT_IO 模式(#22988)。本地存储也能从中获益,但最大的提升还是要看网络存储。

6. 全方位的查询加速

和每次发布一样,大量的工作都花在了让现有查询变得更快上,而你无需做任何改动。举几个亮点:部分聚合下推到 join 之下(#22572),冗余聚合会被复用(#24543),递归 CTE 引擎被重写(#22211),聚合在内存不够时会溢出到磁盘(#24499),Windows CLI 在多线程结果物化方面提速约 2.2 倍(#24036)。

到底能快多少?这里有一个可以在笔记本上跑的微基准:在包含一百万条边的图上做单源可达性查询,用一个普通的递归 CTE来写。

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 倍(!)

行组剪枝(row-group pruning)获得了大幅扩展:min-max 索引(zone map)Parquet Bloom 过滤器现在可以跳过 struct、list、decimal、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)。Lakehouse 格式(DuckLake、Iceberg 以及 S3 上普通的 Hive 分区 Parquet)都是分区式的,能否利用这些分区往往决定了你是扫描整个数据集,还是能跳过其中大部分。在 v2.0 中,规划器和优化器会充分利用已有的分区信息,分区写入也经过了重新设计(#22225#22620)。

7. 存储格式 v2.0

DuckDB v2.0 将默认的存储格式版本提升到 v2.0.0(#22875)。

列元数据现在采用延迟加载方式(#22333),因此宽表的打开速度也更快了。DICT_FSST 字符串压缩方法已默认启用(#23733),删除操作以紧凑形式存储(#24336),存储层在读取时执行更强的损坏校验。简而言之:拥有大型索引和宽表的数据库打开速度更快、内存占用也大幅降低。新的存储格式还支持检查点清理(checkpoint vacuuming),通过增量重映射行 ID 发生变化的条目来压缩带 ART 索引的表,避免全量重建索引(#23653)。

今年晚些时候,ART 索引将采用缓冲区管理(#21458#23605)。这将允许 ART 索引缓冲区在内存压力下被淘汰,并解除 ART 索引必须完全装入内存的限制。

8. 全新的 SQL 解析器

DuckDB 历来使用的是基于 PostgreSQL 衍生而来的解析器。我们认为这样已经足够了:v2.0 正式搭载我们自研的、基于 PEG 的现代化可扩展解析器(#22194),这一思路最早在 2024 年我们关于运行时可扩展解析器的文章中提出过。这一变化与扩展生态紧密相连:扩展现在可以接入语法本身,因此未来会出现暴露全新 SQL 语法的扩展。它还带来了更友好的错误提示,精确定位源代码位置,并提供了首个方言兼容模式:

SET dialect_compatibility_mode = 'spark';

由于我们专门设计为与旧解析器兼容,理论上你不应该察觉任何差异。如果真发现了问题,请提交 issue。

9. 摆脱 ICU 的时区、日历和排序规则

DuckDB 中涉及时区的时间戳、日历和排序规则一直由 ICU 库提供支持。ICU 本身是个不错的库,但我们实际只用了其中很小一部分,却要在每个 DuckDB 发行版中都带上它。在 v2.0 中,ICU 库被彻底移除:icu 扩展现在自行实现时区、日历和排序规则(#24463#24403),时区数据直接从 IANA 数据库构建而来,压缩后约为 45 kB。所有功能都与之前完全一致:

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 s 0.11 s 2.2×
使用 COLLATE de 过滤,500 万行 0.15 s 0.06 s 2.6×

10. 一次编写扩展,自行托管发布

扩展是 DuckDB 最出色的特性之一,但目前大多数扩展(包括我们自家的)都是基于不稳定的 C++ API构建的。这意味着扩展作者必须在每次 DuckDB 发布时重新适配并重新编译,而社区扩展也可能在作者停止维护后悄然消失。DuckDB v2.0 大幅扩展了稳定版 C API 的覆盖范围,使扩展只需编写一次、编译一次、发布一次,就能持续运行下去——基本可以一劳永逸。

为了让这一机制长期可持续,C API 现在由一份声明式的、带版本管理的规范自动生成(#24135):duckdb.hduckdb_extension.h 以及扩展 ABI 中的每个函数,都以 YAML 格式描述在 api_spec/ 目录下,其完整生命周期都有记录,CI 也会验证提交的头文件与规范是否一致,从而杜绝 API 和 ABI 出现偏差。此次发布还带来了统一的符号版本管理(#24435)、自定义内存分配回调(#23945),以及将 C API 扩展静态链接到你的应用中(#22251)。

那么,基于稳定版 C API 构建一个扩展是什么样的体验呢?下面是一个完整的扩展:单个文件,注册一个向量化标量函数,针对 duckdb_extension.h 编译一次即可。

参见下面注册 add_numbers 函数的 C 代码。
#include "duckdb_extension.h"

DUCKDB_EXTENSION_EXTERN

// 一个逐 vector 处理、将两个 BIGINT 相加的 scalar 函数
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 extension

编译得到的二进制文件可以跨 DuckDB 版本持续使用,每次新版本发布时无需重新适配或重新编译。而且在如今各种 AI 工具加持下,构建一个 extension 也变得前所未有的简单。

扩展写好了,下一步该怎么分发?此前 DuckDB 只能从内置仓库(corecore_nightlycommunity 等)安装扩展。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 能读的东西都行:本地路径、httpss3,随便你指定。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 基金会中增设一个利益相关方顾问委员会。该委员会将为 DuckDB、DuckLake 以及 Quack 的开发路线图提供意见,让核心利益相关方有机会参与项目方向的决策。

写在最后

以上只是部分亮点,本文也只是一篇前瞻介绍。在秋季正式发布之前,部分细节可能还会有调整,还有大量新功能和改进未能在此一一展开。DuckDB v2.0 也会带来少量破坏性变更,包括新的默认存储格式以及 lambda 语法过渡的收尾工作,我们将在发布公告中详细介绍。

自 v1.5 发布以来,已经有众多贡献者提交了超过 10,000 次 commit。感谢社区提供了详尽的 issue 报告、反馈和贡献,共同塑造了本次发布。如果想在秋季之前先睹为快,preview 构建版 目前已包含上述大部分功能;万一遇到问题,issue 跟踪系统 就摆在那里。

本文目录

最新文章

Reconciling JSON in DuckDB, One Patch at a Time

在 DuckDB 中逐补丁调和 JSON

2026-08-18 14 分钟 Mustafa Khan Thank You for 40&nbsp;000 Stars on GitHub

感谢 GitHub 上 40 000 颗 Star

2026-08-05 4 分钟 DuckDB 团队 DuckDB 中的异步 I/O:Work、Thread、Work

DuckDB 中的异步 I/O:Work、Thread、Work

2026-07-31 21 分钟 Pedro Holanda 所有博客文章
原始来源: Hacker News

评论 (0)