DuckDB 2.0.0 LTS“Cinnamon Teal”发布
DuckDB 2.0.0 LTS “Cinnamon Teal”
DuckDB 团队发布 2.0.0 LTS 版本“Cinnamon Teal”,支持至少到 2027 年 10 月,包含近 2 万次提交。主要更新包括完善版 Quack 协议与 CONNECT 语句、Parquet 和 CSV 的异步 I/O、一等公民 VARIANT 类型、CLI 的 agent mode、触发器、默认启用的 PEG SQL 解析器,以及可自托管的扩展仓库。
简而言之:DuckDB 团队很高兴地宣布,今天我们发布 DuckDB 2.0.0 LTS 版本,代号“Cinnamon Teal”(Spatula cyanoptera)。这是迄今为止我们规模最大的功能版本,它带来了完善版的 Quack 协议、异步 I/O、一等公民的 VARIANT 类型、agent 模式、触发器、可扩展的 SQL 解析器以及更多功能,这些均基于自 DuckDB 1.5 以来的近 20,000 次提交构建而成。
我们很自豪地发布 DuckDB 2.0.0,代号“Cinnamon Teal”,得名于 Spatula cyanoptera,一种生活在美洲西部、呈醒目红褐色的鸭子。
DuckDB 1.0 主要关注稳定性和向后兼容性,而 DuckDB 2.0 首先是一个功能版本。 在这篇博客文章中,我们总结了亮点,然后介绍破坏性变更以及如何迁移。 我们在“DuckDB v2.0 预览”中展示了许多新功能,该文对每项功能进行了更深入的介绍。 完整的发布说明请参阅 GitHub 上的发布页面。
提示 要安装新版本,请访问安装页面。 部分客户端(例如 CLI 和 Python)已经可用。其他客户端库(例如 Go、R、Java)以及部分扩展可能需要几天时间才能发布,因为需要额外的变更和审查轮次。
本次发布后,目前有三条 DuckDB 版本线可用。 DuckDB 2.0 是长期支持(LTS)版本,支持至少到 2027 年 10 月。 上一个 LTS 版本 1.4 仍受支持,直到 2026 年 11 月下旬。 为确保平稳过渡并有足够时间进行测试,我们将 DuckDB 1.5 的支持期延长至 2027 年 3 月,实际上也将其转变为 LTS 版本。 详情请参阅发布日历。
为了帮助你在文档中了解所有新功能,我们还发布了“2.0 文档新内容”页面,将下面的每项功能链接到其文档。
DuckDB 作为服务器
DuckDB 从第一天起就是进程内数据库。
在 5 月发布的 DuckDB 1.5.3 中,我们引入了 Quack 协议,让你可以在传统的客户端-服务器架构中运行 DuckDB,同时还引入了 quack 扩展,它实现了 DuckDB 的原生协议,因此任何 DuckDB 进程都可以提供其数据库服务。
Quack 与 CONNECT
DuckDB 2.0.0 带来了完善版的 Quack 协议,在性能和稳定性方面有大幅提升。它还引入了新的 CONNECT 语句,允许通过单条命令附加并切换到远程数据库,从而提升易用性:
DuckDB 服务器
CALL quack_serve(
token = 'my_token'
);
CREATE TABLE events
(date DATE, name VARCHAR);
INSERT INTO events VALUES
('2027-04-07',
'DuckCon #8');
quack:
DuckDB 客户端
ATTACH 'quack:localhost'
AS qk (TOKEN 'my_token');
CONNECT qk;
SELECT * FROM events;
┌────────────┬────────────┐
│ date │ name │
│ date │ varchar │
├────────────┼────────────┤
│ 2027-04-07 │ DuckCon #8 │
└────────────┴────────────┘
为简单起见,此示例在同一台机器上运行服务器和客户端。默认情况下,服务器仅监听 localhost。要接受来自其他机器的连接,请让服务器保持在 localhost 上,并将其置于终止 TLS 的反向代理之后。allow_other_hostname 选项会直接暴露服务器,仅用于测试。
CONNECT 语句并不局限于 Quack:它会把你的会话指向任何支持它的远程数据库,包括 PostgreSQL 和 MySQL。对于这些数据库,新的远程下推优化器会直接把查询发送到远程服务器,而不是通过网络拉取整张表。
CLI 中的 Quack
CLI 还提供了用于搭建 Quack 客户端-服务器对的快捷方式:新增的 -serve 和 -connect 参数 可以直接从命令行启动 Quack 服务器或连接到一台服务器:
CLI 服务器
duckdb -serve
CREATE TABLE t AS
SELECT 42 AS x;
quack:
CLI 客户端
duckdb -connect
FROM t;
┌───────┐
│ x │
│ int32 │
├───────┤
│ 42 │
└───────┘
性能
异步 I/O
DuckDB 2.0 为读取 Parquet 和 CSV 文件引入了异步 I/O。I/O 层现在可以独立于查询处理层进行扩展,在后台发起读取并保持多个请求同时在途。这意味着远程读取的并行度大大提高,在 s3:// 等对象存储上的查询速度也显著加快。
我们在一篇专门的博客文章中详细描述了这一设计。 在那篇文章中,我们展示了在启用和不启用异步 I/O 的情况下,一次查询过程中的网络吞吐量:
网络吞吐量。利用率越高、运行时间越短越好。
VARIANT 类型成为一等公民
VARIANT 类型可存储 JSON 等半结构化、无模式的数据,同时保持列式执行的性能。
因此你可以把它看作增强版的 JSON:VARIANT 列可以在每一行存储形状不同的数据,但与 JSON 不同,它不是文本格式。
DuckDB 1.5 已经引入了 VARIANT,但 DuckDB 2.0 将其完全集成。DuckDB 2.0 会检测数据中的常见结构并对其进行“shredding”,因此压缩效果好、执行速度快。该流水线端到端贯通,从存储直接进行 shredded 执行,到 Parquet 的 shredded VARIANT 读写,并由一系列 variant_* 函数提供支持。例如:
CREATE TABLE events (payload VARIANT);
INSERT INTO events
VALUES
('{"user": {"id": 42, "tags": ["a", "b"]}}'::JSON::VARIANT),
('{"event": "click", "x": 10, "y": 20}'::JSON::VARIANT);
SELECT variant_type(payload), variant_keys(payload)
FROM events;
┌───────────────────────┬───────────────────────┐
│ variant_type(payload) │ variant_keys(payload) │
│ varchar │ varchar[] │
├───────────────────────┼───────────────────────┤
│ OBJECT │ [user] │
│ OBJECT │ [event, x, y] │
└───────────────────────┴───────────────────────┘
CLI 中的 Agent 模式
Claude Code、Codex 和 Cursor 等编码代理越来越多地代表用户驱动 DuckDB CLI。2.0 CLI 会检测自己是否由代理运行,并切换到代理模式:它会打印紧凑的 Markdown 表格而不是带填充的方框,在结果被截断时明确说明,提前停止失控的查询,并以 JSON 报告错误。
长时间运行的 SQL 查询会在运行前报告其预期开销,以便代理决定是否等待。在我们针对 TPC-H 的实验中,代理模式使模型需要读取的 DuckDB 输出量减少了 59%。你可以使用 -agent 标志手动测试时将其开启,或指示你的代理使用 -no-agent 标志将其关闭。
可扩展性
全新的 SQL 解析器
DuckDB 一直使用源自 PostgreSQL 的解析器。在 2.0 中,我们自己的现代化、可扩展的基于 PEG 的解析器(它作为可选功能在 DuckDB 1.5 中首次亮相)成为默认解析器。扩展现在可以接入语法本身(这个想法我们最早在关于运行时扩展解析器的研究中探索过),因此可以期待出现暴露全新 SQL 语法的扩展。错误消息现在会附带精确的源代码位置。详情请参阅 PEG 解析器文档。新解析器设计为与旧解析器兼容,因此你应当不会察觉到这次替换。如果你察觉到了,请提交 issue。同时,你可以通过运行 SET enable_legacy_parser = true; 使用 legacy_parser 扩展切换回旧解析器。
旧解析器自 2018 年 DuckDB 的首次提交起就一直为其服务。它的语法由 YACC/Bison 生成,产生的是 LALR(1) 解析器,因此随着 DuckSQL 的增长,即使是很小的新增也可能与现有规则冲突,引发 shift/reduce 或 reduce/reduce 冲突。PEG 语法通过按书写顺序尝试备选方案来避免这一点,因此添加语法不再有破坏无关规则的风险。Python 本身也在 3.9 版本中转向基于 PEG 的解析器,以获得同样的灵活性。
一次编写扩展,自行托管
DuckDB 2.0 带来了经过改造的 C API v2,它比前代强大得多,并向扩展暴露了文件系统和表函数等组件。在将其标记为稳定之前,我们希望在实际场景中测试它,所以请试用一下并向我们发送反馈!
你还可以注册并托管自己的受信任扩展仓库,这样组织就可以托管并签名自己的扩展,并让它们像内置扩展一样安装和加载:
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY my_repo
WITH PREFIX 'https://extensions.example.org';
INSTALL AND LOAD my_ext FROM my_repo;
仓库由一个名称、一个 URL 前缀以及一个或多个受信任用于签名该仓库所提供扩展的 RSA 公钥组成。前缀可以指向 DuckDB 能读取的任何内容:本地路径、https、s3,等等。在 CREATE 时,DuckDB 会获取仓库的公钥并将其固定到仓库定义中,同时打印每个密钥的 SHA-256 指纹,以便你与通过带外方式公布的指纹进行比对。
此功能仍在开发中,其语法可能会在未来版本中发生变化。更多详情请参阅扩展仓库页面。
核心扩展更新
DuckDB 的核心扩展也带来了多项新功能、修复和性能优化。这些内容太多,无法在此全部列出,因此我们只重点介绍其中几项,并将在未来几周通过专门的博客文章进行介绍:
- 我们发布了 DuckLake 1.1,其中包括新的规范和参考实现。
- Iceberg 扩展力求成为功能完整的实现。为此,它新增了 REST catalog
ATTACH选项,可从表的命名空间推导其位置,或从 catalog 返回的metadata-location文件加载表元数据。还有更多选项用于控制 URL 前缀和命名空间分隔符。 - Delta 扩展可以使用
CREATE TABLE创建 Delta 表(包括分区和 Delta 表属性),支持时间旅行,并改进了对VARIANT的支持。
SQL 功能
触发器
DuckDB 2.0 带来触发器:CREATE TRIGGER 语句支持 BEFORE 和 AFTER 触发器、FOR EACH ROW 和 FOR EACH STATEMENT、通过 REFERENCING OLD/NEW TABLE 实现转换表、每个事件多个触发器、被触发表上的 RETURNING 以及 DROP TRIGGER。经典用例是审计表,其中触发器记录响应表修改而发生的变更。某些组合,例如 BEFORE ... FOR EACH ROW 和 UPDATE ... FOR EACH ROW 触发器,尚不支持;请参阅限制。
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;
触发器还可以增量维护物化视图!例如,要跟踪表 age_statistics 中按国籍划分的人的平均年龄,你可以使用两个触发器——一个用于插入操作,另一个用于删除操作——并将总和与计数与平均值一起存储。
要查看更多详细信息,请点击此处。
CREATE TABLE people (name VARCHAR, nationality VARCHAR, age INTEGER);
CREATE TABLE age_statistics (
nationality VARCHAR PRIMARY KEY,
sum_age INTEGER,
num_people INTEGER,
average_age DOUBLE
);
插入时,我们按国籍聚合新行并将它们合并到 age_statistics 中,为每个新国籍添加一行:
CREATE TRIGGER people_insert
AFTER INSERT ON people
REFERENCING NEW TABLE AS n
FOR EACH STATEMENT
MERGE INTO age_statistics s
USING (
SELECT nationality, sum(age) AS sum_age, count(*) AS num_people
FROM n
GROUP BY nationality
) d
ON s.nationality = d.nationality
WHEN MATCHED THEN UPDATE SET
sum_age = s.sum_age + d.sum_age,
num_people = s.num_people + d.num_people,
average_age = (s.sum_age + d.sum_age) / (s.num_people + d.num_people)
WHEN NOT MATCHED THEN INSERT
VALUES (d.nationality, d.sum_age, d.num_people, d.sum_age / d.num_people);
删除时,我们减去已删除的行,并在某个国籍的最后一个人离开后删除该国籍:
CREATE TRIGGER people_delete
AFTER DELETE ON people
REFERENCING OLD TABLE AS o
FOR EACH STATEMENT
MERGE INTO age_statistics s
USING (
SELECT nationality, sum(age) AS sum_age, count(*) AS num_people
FROM o
GROUP BY nationality
) d
ON s.nationality = d.nationality
WHEN MATCHED AND s.num_people = d.num_people THEN DELETE
WHEN MATCHED THEN UPDATE SET
sum_age = s.sum_age - d.sum_age,
num_people = s.num_people - d.num_people,
average_age = (s.sum_age - d.sum_age) / (s.num_people - d.num_people);
让我们看看人们的来来去去:
INSERT INTO people VALUES
('Alice', 'US', 34), ('Bram', 'NL', 27), ('Csilla', 'HU', 45),
('Dave', 'US', 58), ('Eva', 'NL', 19), ('Femke', 'NL', 41);
FROM age_statistics ORDER BY nationality;
┌─────────────┬─────────┬────────────┬─────────────┐
│ nationality │ sum_age │ num_people │ average_age │
│ varchar │ int32 │ int32 │ double │
├─────────────┼─────────┼────────────┼─────────────┤
│ HU │ 45 │ 1 │ 45.0 │
│ NL │ 87 │ 3 │ 29.0 │
│ US │ 92 │ 2 │ 46.0 │
└─────────────┴─────────┴────────────┴─────────────┘
DELETE FROM people WHERE name = 'Alice';
DELETE FROM people WHERE nationality = 'HU';
FROM age_statistics ORDER BY nationality;
┌─────────────┬─────────┬────────────┬─────────────┐
│ nationality │ sum_age │ num_people │ average_age │
│ varchar │ int32 │ int32 │ double │
├─────────────┼─────────┼────────────┼─────────────┤
│ NL │ 87 │ 3 │ 29.0 │
│ US │ 58 │ 1 │ 58.0 │
└─────────────┴─────────┴────────────┴─────────────┘
触发器让我们离物化视图更近了一步,这是我们路线图“未来工作”部分中长期存在的一项。
SQL 方言新增内容
一如既往,DuckDB 的友好 SQL 方言不断扩展。本发布周期中的几个亮点:
$variable简写允许你在大多数允许表达式的位置编写$x,而不是getvariable('x')。- 数据修改 CTE 允许你将
INSERT、UPDATE、DELETE和COPY用作管道步骤。 NEAREST连接将 top-k 相似性搜索转变为连接子句,便于向量和嵌入工作负载。- 嵌套模式允许模式中包含模式,例如
CREATE SCHEMA finance.reports。 - JSON 变更函数
json_set、json_insert、json_replace和json_remove返回 JSON 文档的修改副本(另请参阅我们关于 JSON 补丁的博客文章)。
还有更多:DuckDB 2.0.0 支持 SQL 标准的 FETCH FIRST,并允许在 GROUP BY 子句中使用 UNNEST。它还确保在匹配多行的情况下具有明确定义的 MERGE 和 UPDATE ... FROM 语义。
迁移到 2.0
DuckDB 2.0 带来了一小部分破坏性变更。对于大多数用户来说,升级很简单,但运行许多 DuckDB 进程的复杂部署需要更加小心。存储格式最有可能成为问题来源:2.0 之前的 DuckDB 版本无法读取 DuckDB 2.0 创建的数据库文件。DuckDB 2.0 仍然可以读写你现有的数据库文件,并且这些文件仍可被旧版本读取。我们建议在切换生产环境之前先在影子系统上测试 2.0.0。而且,如果你的工作负载是关键任务,你可能想等待第一个补丁版本,将于 11 月发布。
有关完整详细信息,请参阅迁移指南。
内部实现与弃用
编译器要求
在 DuckDB 长期使用 C++11 之后,DuckDB 2.0 现在要求使用 C++17 编译器。
我们还切换为 Clang 作为发布构建的默认编译器。在 Linux 和 macOS 上,DuckDB 及其核心扩展均使用 Clang 构建。在 Windows 上,CLI 使用 Clang 构建,而大多数核心扩展仍使用 MSVC 构建。未来我们可能会将更多 Windows 构建迁移到 Clang,但这需要进一步的更改。
无需 ICU 的时区、日历和排序规则
时区感知的时间戳、日历和排序规则一直由 ICU 库提供支持。在 2.0 中,ICU 库已被完全移除:icu 扩展现在自行实现时区、日历和排序规则,时区数据直接基于 IANA 数据库构建,并压缩至约 45 kB。
一切功能都照常运行,原生实现还更快。如果你发现任何问题,请告诉我们!
弃用
Lambda 语法
旧的单箭头 lambda 语法(x -> x + 1)现在默认禁用。请改用 Python 风格的语法:
SELECT list_transform([1, 2, 3], lambda x: x + 1);
你可以通过 SET lambda_syntax = 'enable_single_arrow'; 临时重新启用旧语法。该标志和单箭头语法都将在 v2.1 中完全移除。
合并构建
合并构建将 DuckDB 的全部源代码打包成单个头文件和单个源文件,现已弃用。合并构建的体积已变得过大,在大多数情况下已不再具有实际可用性。
逐步淘汰 macOS x86_64
我们正在逐步将 osx_amd64 从完全支持的平台列表中移除。
目前,DuckDB 和核心扩展仍会构建并支持该平台,但社区扩展不再为该平台构建。DuckDB 2.1 将停止支持 osx_amd64。
总结
以上只是部分亮点,本次发布还有更多功能和改进。自 DuckDB 1.5 以来,众多贡献者提交了近 20,000 次提交,带来了更多小功能以及性能和易用性方面的改进。完整的发布说明可在 GitHub 上找到。
我们感谢社区提供详细的 issue 报告和反馈,这些推动了本次发布,也特别感谢我们的贡献者!
在 NVIDIA Vera 上对 DuckDB v2.0-dev 进行基准测试
Gábor Szárnyas
从 JSON 字符串到类型化列
DuckDB 团队
Iceberg 表里有什么?
DuckDB 团队
来源:Hacker News · duckdb.org