DuckDB 2.0:一个功能版本,同时把 DuckDB 变成了数据库服务器

·阅读约12分钟·Evergreen Tools Team

💡 工具推荐: 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 月,所以长期看这是你应该落地的目标版本。