DuckDB 2.0:一个功能版本,同时把 DuckDB 变成了数据库服务器
💡 工具推荐: CSV 转 JSON、JSON 格式化、SQL 格式化
DuckDB 2.0.0 在 2026 年 10 月 21 日发布,代号 Cinnamon Teal,取自一种分布在西半球、羽色红褐的鸭子。官方说这是他们至今最大的功能版本,背后接近两万个提交。这句话值得留意:DuckDB 1.0 的主题是稳定性与向后兼容,而 2.0 首先是一个功能版本。功能版本通常意味着迁移工作,而这次迁移的时间窗口是明确的——1.4 LTS 的支持到 2026 年 11 月 17 日结束。所以这篇文章分两半:新东西值不值得用,以及什么时候必须动。我把 2.0.0 装起来,把关键功能真跑了一遍,下面贴的是实际输出,不是文档抄写。
Quack 协议才是 2.0 真正的头条
一、真正的头条是 Quack,不是某个跑分
DuckDB 的自我定位一直是嵌入式数据库,进程内运行,没有服务器。这次它把这条线挪了。Quack 协议在 1.5.3(今年 5 月)引入,让 DuckDB 能跑成传统的客户端-服务器架构;2.0.0 交付的是一个打磨过的版本,性能与稳定性都有实质改进,并且新增 CONNECT 语句——一条命令完成 attach 与切换。官方例子很干净:服务端 CALL quack_serve(token = 'my_token'),客户端 ATTACH 'quack:localhost' AS qk (TOKEN 'my_token') 然后 CONNECT qk。CLI 也给了 -serve 和 -connect 两个快捷参数。更重要的是,CONNECT 不只对 Quack 有效,对 PostgreSQL 和 MySQL 也成立,而且配套的远程下推优化器会把查询直接发到远端服务器,而不是把表拉过来。这一点决定了很多团队的使用姿势:主库留在 PG,分析直连过去跑。
# 1) What I actually installed and ran. Python 3, a throwaway venv.
python3 -m venv /tmp/duck200venv
/tmp/duck200venv/bin/pip install "duckdb==2.0.0"
$ /tmp/duck200venv/bin/python -c "import duckdb; print(duckdb.__version__)"
2.0.0
# The official line is an LTS: supported until at least October 2027.
# Release calendar as published on 2026-10-21:
# 2.0.0 LTS supported until at least October 2027
# 1.5 extended to March 2027 (effectively also LTS)
# 1.4 LTS supported until November 17, 2026二、VARIANT 从「有」变成「好用」,但转换语法有个坑
VARIANT 在 1.5 就有,2.0 才真正整合完成。它的定位是把 JSON 这类半结构化数据存成列式,官方说法是「JSON on steroids」:每一行的结构可以不一样,但它不是文本格式。2.0 会检测数据里的共同结构并做 shredding,于是既能压得小,又能执行得快;读写 Parquet 走的是 shredded 路径,背后是一整套 variant_* 函数。我第一次试的时候直接踩了坑:往 VARIANT 列里插一个 JSON 字符串字面量,不会自动解析,取字段全是 NULL。必须先显式走一遍 JSON 转换,也就是 '{"a":1}'::JSON::VARIANT。改对之后 variant_type() 返回 OBJECT,variant_keys() 返回实际的键列表,点号取字段也正常,缺字段的行返回 NULL 而不是报错——这一点比很多 JSON 扩展友好。
# 2) VARIANT, which is the feature most people will actually touch.
# Note the cast: a plain string literal is NOT parsed into VARIANT.
# It has to go through JSON first. This bit me immediately.
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;
# Real output from my run:
# ('OBJECT', ['user'])
# ('OBJECT', ['event', 'x', 'y'])三、触发器:现在可以真的维护增量聚合了
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 这两类组合还不支持。最实用的场景是增量维护物化视图。官方给的例子是用两个触发器(插入一个、删除一个)维护一张按国籍统计平均年龄的表,同时存 sum 和 count,这样平均值可以增量算。我按那个例子实跑了一遍:插入四个人、三个国籍之后,age_statistics 里是 FI 55/1/55.0、UK 77/2/38.5、US 45/1/45.0。注意那个 38.5——它是 (36+41)/2 增量合并出来的,不是全表重算。这一条对分析场景价值很大,因为它把「物化视图要定时全刷」变成了「变更即更新」。
VARIANT 让半结构化数据继续走列式
# 3) Triggers. DuckDB 2.0 has real ones now: BEFORE/AFTER,
# FOR EACH ROW / FOR EACH STATEMENT, REFERENCING OLD/NEW TABLE,
# multiple triggers per event, DROP TRIGGER.
# Documented limitation: BEFORE ... FOR EACH ROW and
# UPDATE ... FOR EACH ROW are not supported yet.
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) * 1.0 / (s.num_people + d.num_people)
WHEN NOT MATCHED THEN INSERT VALUES
(d.nationality, d.sum_age, d.num_people, d.sum_age * 1.0 / d.num_people);
# Insert 4 people, 3 nationalities. Real output, ordered by nationality:
# ('FI', 55, 1, 55.0)
# ('UK', 77, 2, 38.5)
# ('US', 45, 1, 45.0)
# A running aggregate maintained by a trigger, not by application code.四、agent 模式和异步 I/O:两个容易被忽略但很实用的改动
agent 模式是这次最有意思的小设计。官方说 Claude Code、Codex、Cursor 这类编码 agent 越来越多地代替用户驱动 DuckDB CLI,于是 2.0 的 CLI 会检测自己是不是被 agent 启动的,然后切到 agent 模式:打印紧凑的 Markdown 表格而不是带边框的方框、明确说明结果被截断了、提前中止失控的查询、把错误以 JSON 形式报出来。这四条恰好是 agent 用 CLI 时最痛的四个点,尤其是「截断要说清楚」——静默截断会让 agent 基于不完整数据得出结论。另一项是异步 I/O:读 Parquet、CSV 和 DuckDB 原生存储格式时,I/O 层现在与查询处理层解耦,后台发读、同时保持多个请求在飞。对本地文件意义有限,对 s3:// 这类对象存储是数量级的差别。
# 4) Quack: the in-process database learns to be a server.
# Official example, kept verbatim because the shape is the point.
# DuckDB server
CALL quack_serve(token = 'my_token');
CREATE TABLE events (date DATE, name VARCHAR);
INSERT INTO events VALUES ('2027-04-07', 'DuckCon #8');
# DuckDB client
ATTACH 'quack:localhost' AS qk (TOKEN 'my_token');
CONNECT qk;
SELECT * FROM events;
# ┌────────────┬──────────────┐
# │ date │ name │
# │ date │ varchar │
# ├────────────┼──────────────┤
# │ 2027-04-07 │ DuckCon # 8 │
# └────────────┴──────────────┘
# CLI shortcuts: duckdb -serve / duckdb -connect
# The server listens on localhost by default. Exposing it directly needs
# allow_other_hostname, which the docs say is for testing only.五、迁移:时间窗是明确的,别等到 11 月
版本路线官方写得很清楚:2.0 是 LTS,支持到至少 2027 年 10 月;上一个 LTS 1.4 支持到 2026 年 11 月 17 日;1.5 被延长到 2027 年 3 月,实际上也成了 LTS。这个安排给了两段缓冲,但第一段只剩三周。迁移清单我建议按这个顺序:先读破坏性变更那一节而不是功能列表;然后把 duckdb 精确锁到 2.0.0,同时检查扩展锁文件(Delta、Iceberg、spatial、httpfs 都要过一遍);再验三件事——VARIANT 写进 Parquet 再读回来、variant_type() 是否一致;触发器是不是按语句触发而非按行触发;CONNECT 先连只读副本再连主库。最后一句实话:这次升级不是「必须马上」,但 11 月 17 日之后你就在用一个不再收安全补丁的版本,这个风险比功能本身重要。
# 5) Migration plan, written the way the release notes force you to write it.
migration_2_0:
deadline_hard: "2026-11-17" # 1.4 LTS support ends
comfortable: "2027-03" # 1.5 extended, effectively LTS
target: "2.0.0 LTS, supported to at least 2027-10"
before_you_start:
- "read the 12-ish breaking changes, not just the feature list"
- "pin duckdb==2.0.0 in requirements, and in any extension lockfile"
- "check every extension you load: Delta, Iceberg, spatial, httpfs"
verify:
- "VARIANT round-trips: write Parquet, read it back, compare variant_type()"
- "triggers fire once per statement, not once per row, unless you meant it"
- "CONNECT to a read replica before you point it at primary"
measured_locally:
"10M-row aggregate via FROM-first syntax": "0.018s"
📌 常见问题 FAQ
DuckDB 2.0.0 是什么时候发布的?
2026 年 10 月 21 日,代号 Cinnamon Teal(Spatula cyanoptera)。官方称这是至今最大的功能版本,自 DuckDB 1.5 以来接近两万个提交。它是一个 LTS 版本,支持到至少 2027 年 10 月。
Quack 协议是什么?
Quack 是 DuckDB 的原生协议,在 1.5.3(2026 年 5 月)引入,让 DuckDB 可以跑成传统的客户端-服务器架构,任意 DuckDB 进程都能提供数据库服务。2.0.0 交付的是打磨过的版本,并新增 CONNECT 语句在一条命令里完成 attach 与切换。
VARIANT 和 JSON 类型有什么区别?
按官方说法,VARIANT 存的是半结构化、无模式的数据,但不像 JSON 那样是文本格式。2.0 会检测数据中的共同结构并做 shredding,因此压缩效果好、执行快,Parquet 读写也走 shredded 路径。实践上要注意:字符串字面量要显式 ::JSON::VARIANT 转换后才会被解析。
2.0 的触发器有什么限制?
文档明确列出:BEFORE ... FOR EACH ROW 和 UPDATE ... FOR EACH ROW 这两类组合尚不支持。已支持的是 BEFORE/AFTER、FOR EACH ROW/STATEMENT、通过 REFERENCING OLD/NEW TABLE 访问变化表、同一事件多个触发器、被触发表的 RETURNING,以及 DROP TRIGGER。
我现在必须升级吗?
取决于你的版本。1.4 LTS 的支持在 2026 年 11 月 17 日结束,在那之后继续用意味着不再收到修复。1.5 被延长到 2027 年 3 月,时间宽裕一些。2.0 是 LTS,支持到至少 2027 年 10 月,所以长期看这是你应该落地的目标版本。