DuckDB 2.0.0 LTS(Cinnamon Teal)正式发布
DuckDB 2.0.0 LTS “Cinnamon Teal”
DuckDB 团队发布 2.0.0 LTS 版本,代号 Cinnamon Teal,支持周期至 2027 年 10 月。
DuckDB 2.0 LTS 在 server 化、异步 I/O 与 CLI 的 agent 模式上都有具体变化,为数据基础设施团队评估迁移成本与代理工具集成提供了直接依据。
TL;DR: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 则首先是一次功能版本发布。 在这篇博客文章中,我们将先总结亮点,再讲解破坏性变更及迁移方法。 我们在 “A Preview of 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 版本。 详情请参见 发布日历。
为方便您查阅所有新特性的文档,我们还发布了一页 “What's New in the Docs for 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。对于这些数据库,新的远程下推优化器会将查询直接发送到远程服务器,而不是通过网络拉取表。
命令行中的 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 会检测数据中的常见结构并将其“拆分”,从而实现良好的压缩效果和快速执行。整条流水线端到端贯通,从直接基于存储的拆分式执行,一直贯穿到面向 Parquet 的拆分式 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] │
└───────────────────────┴───────────────────────┘
命令行中的智能体模式
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 解析器文档。新解析器旨在与旧解析器兼容,因此您应该不会察觉到这个替换。如果您察觉到了,请提交问题单。在此期间,您可以使用 legacy_parser 扩展,通过运行 SET enable_legacy_parser = true; 切换回旧解析器。
旧解析器从 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 目录
ATTACH选项,可根据表的命名空间推导表的位置,或从目录返回的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 月发布。
有关完整详细信息,请参阅迁移指南。
内部实现与弃用项
编译器要求
在长期使用 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 上找到。
我们要感谢社区成员提供的详细问题报告和反馈,它们推动了此次发布;也要特别感谢我们的贡献者!
在 NVIDIA Vera 上对 DuckDB v2.0-dev 进行基准测试
Gábor Szárnyas
从 JSON 字符串到类型化列
DuckDB 团队
Iceberg 表中有什么?
DuckDB 团队
来源:Hacker News · duckdb.org