教程·阅读约 4 分钟·
DuckDB v2.0 抢先看:10 个新特性逐个拆解,含完整 SQL 示例

DuckDB v2.0 抢先看:10 个新特性逐个拆解,含完整 SQL 示例

DuckDB 官方发布了 v2.0 预览:服务端模式、VARIANT 类型、触发器、全新 SQL 解析器、存储格式升级——10 个新特性带代码示例逐个讲清楚。

原文来源:DuckDB 官方博客 — DuckDB v2.0(代号 Cyanoptera)预览:从 SQL 层特性到引擎底层,10 个新特性逐个拆解,附带完整代码示例。

这次升级有多大

DuckDB v2.0 将于今年秋季正式发布,代号 "Cyanoptera"(取自美洲的一种红褐色鸭子)。这是一次真正的大版本升级:新 SQL 解析器、新默认存储格式、重构的 C API,以及少量经过精心挑选的破坏性变更。从 3 月发布 v1.5 以来,v2.0 积累了超过 10,000 个提交。

官方博客的说法是:去年是 lakehouse 之年,这次发布开启了"DuckDB 作为服务器"的一年。下面按从 SQL 层到引擎底的顺序,逐个拆解 10 个新特性。

—— 广告 ——

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

DuckDB 从第一天起就是进程内数据库,但用户一直在要求客户端/服务器模式。v2.0 通过 quack 扩展实现:任何 DuckDB 进程都可以通过网络提供数据库服务,其他 DuckDB 用新的 CONNECT 语句挂载并路由查询。

服务端启动:

code
CALL quack_serve(token = 'my_token');

客户端连接:

code
ATTACH 'quack:server.example.com' AS qk (TOKEN 'my_token');
CONNECT qk;
SELECT count(*) FROM events;  -- 在服务器上执行,结果流式返回
DISCONNECT;

CONNECT 不限于 Quack——新的远程下推优化器可以把 SQL 直接推送到 PostgreSQL 和 MySQL 执行,而不是把整表拉过来:

code
CONNECT 'postgres://localhost/mydb';
SELECT count(*) FROM orders;  -- 在 PostgreSQL 服务器上执行
DISCONNECT;

DuckDB 其实从第一天起就是带完整 MVCC 和事务隔离的多连接事务数据库,只是单用户场景下很少用到。客户端/服务器模式终于让这套机制在多租户、长期运行的部署中发挥作用,v2.0 也因此在指标、日志和可观测性上做了大量工作(如指标层重构)。

2. VARIANT 成为一等公民

VARIANT 类型在 v1.5 中引入,可以理解为"打了激素的 JSON":每一行的数据形状都可以不同,但不像 JSON 那样以文本存储——DuckDB 会自动检测半结构化数据里的共同结构并"切碎"(shred)存储,压缩好、查询快,还不用声明 schema。非常适合实时日志摄入这类"结构相似但不断演化"的 JSON 流。

v2.0 让这条管线端到端跑通:存储直接执行切碎数据、扫描时提取下推、Parquet 的切碎读写,以及一整套 variant_* 函数:

code
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. 触发器

触发器是呼声极高的长期功能请求,v2.0 一次性给全:BEFOREAFTER 触发器、FOR EACH ROWFOR EACH STATEMENTREFERENCING OLD/NEW TABLE 过渡表、每个事件多个触发器、RETURNING,以及 DROP TRIGGER

经典用例是审计表:

code
CREATE TABLE target (id INTEGER, val INTEGER);
CREATE TABLE audit (id INTEGER, old_val INTEGER, new_val INTEGER);
 
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;
 
INSERT INTO target VALUES (1, 10), (2, 20);
UPDATE target SET val = val * 10 WHERE id <= 2;
SELECT * FROM audit;
-- id | old_val | new_val
-- 1  | 10      | 100
-- 2  | 20      | 200

4. SQL 方言扩展

这一版新增了不少 SQL 语法,挑几个亮点:

NEAREST 连接#24137):top-k 相似度搜索直接变成连接子句,对向量/embedding 工作负载很友好:

code
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 等):INSERTUPDATEDELETECOPY 可以作为管线步骤使用:

code
WITH moved AS MATERIALIZED (
    DELETE FROM staging RETURNING *
)
INSERT INTO archive SELECT * FROM moved;

嵌套 schema#23492):schema 里可以有 schema:

code
CREATE SCHEMA finance;
CREATE SCHEMA finance.reports;
CREATE TABLE finance.reports.q3 (revenue DECIMAL);

变量语法#21194):$x 可以在任何表达式位置使用,不再需要 getvariable(...)

code
SET VARIABLE threshold = 100;
SELECT * FROM orders WHERE amount > $threshold;

JSON 修改函数#23786):json_setjson_insertjson_replacejson_remove 终于可以原地修改 JSON 文档:

code
SELECT json_set('{"a":1}', '$.b', '2');
-- {"a":1,"b":2}

带 USING KEY 聚合的递归 CTE#19481):纯 SQL 就能写迭代算法,背后是重写的递归 CTE 引擎:

code
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;

另外还有标准化的 FETCH FIRST 2 ROWS ONLYOVERLAY()GROUP BY 里的 UNNEST、多匹配行的 MERGE/UPDATE ... FROM 语义等。

5. 异步 I/O

与对象存储(S3 等)交互是 DuckDB 的核心场景。以前是同步访问,限制了速度;v2.0 在整个引擎中引入异步 I/O,I/O 层与查询处理层独立扩展,网络存储上的远程读取并行度大幅提升。Parquet 先行(#23662),CSV(#23961)和 DuckDB 自己的文件格式(#24654)随后跟上,还支持异步 Parquet 写入和新的 MMAPDIRECT_IO 模式。本地存储也有小幅提升,但网络存储才是收益大头。

6. 全面提速

  • 递归 CTE 引擎重写#22211)。官方给了一个笔记本可跑的微基准:百万条边的图上做单源可达性查询,v1.5.4 用时 4.90 秒,v2.0 预览版 0.12 秒——约 40 倍提速
  • 部分聚合下推到连接之下#22572),冗余聚合复用(#24543)。
  • 聚合超出内存时落盘#24499)。
  • 行组剪枝大幅扩展:min-max 索引(zone maps)和 Parquet Bloom filter 现在能跳过 structs、lists、decimals、UUID、IN 过滤,甚至函数谓词:
code
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):对 DuckLake、Iceberg、Hive 分区 Parquet 等 lakehouse 格式,规划器会利用现有分区跳过大部分数据。

7. 存储格式 v2.0

默认存储格式升到 v2.0.0(#22875),头号变化是 buffer-managed ART 索引#21458):索引不再常驻内存,大索引表秒开,索引按需分页载入。列元数据改为惰性加载(#22333),宽表打开更快;DICT_FSST 字符串压缩默认开启(#23733),删除记录存储更紧凑,读时校验更强。一句话:大索引、宽表的数据库打开更快、内存占用更少

8. 全新 SQL 解析器

DuckDB 一直用派生自 PostgreSQL 的解析器,v2.0 换成了自研的现代 PEG 解析器(#22194)。意义在于:

  • 扩展可以挂钩语法本身——未来会出现暴露全新 SQL 语法的扩展
  • 更精确的报错信息和源码位置
  • 第一个方言兼容模式:
code
SET dialect_compatibility_mode = 'spark';

官方说解析器设计上兼容旧版,正常使用应该感知不到变化;如果你感知到了,请去提 issue。

9. 去掉 ICU:时区、日历、排序规则原生实现

DuckDB 的时区、日历和排序规则一直由 ICU 库提供,但只用了它的一小片功能,却要在每个发行版里都带着它。v2.0 里 ICU 被彻底移除,icu 扩展自己实现了这些能力(#24463),时区数据直接取自 IANA 数据库并压缩到约 45 kB:

code
SELECT '2026-08-14 12:00:00'::TIMESTAMPTZ AT TIME ZONE 'Europe/Paris';
SELECT * FROM names ORDER BY name COLLATE de;

体量更小、更新更容易,还更快:MacBook 上把 2500 万行时间戳转时区,v1.5.4 用时 0.24 秒,v2.0 原生实现 0.11 秒(2.2 倍);500 万行德文排序过滤,0.15 秒降到 0.06 秒(2.6 倍)。

10. 扩展只写一次,自己托管

以前大多数扩展(包括官方扩展)都构建在不稳定的 C++ API 上,每次 DuckDB 发版都要重新适配构建,社区扩展也可能因为作者不跟进而悄悄消失。v2.0 把稳定的 C API 扩展到"一次编写、一次构建、一次发布、永久可用"的程度。

为此 C API 改为从声明式、带版本号的规范自动生成(#24135):duckdb.hduckdb_extension.h 和扩展 ABI 的每个函数都在 api_spec/ 目录的 YAML 里描述,CI 校验头文件与规范一致,API 和 ABI 再也不会漂移。还带来统一符号版本管理、自定义分配处理器、C API 扩展静态链接进应用等能力。官方给了一个完整的最小扩展示例——单个文件注册一个向量化标量函数:

code
#include "duckdb_extension.h"
 
DUCKDB_EXTENSION_EXTERN
 
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;
}
code
LOAD add_numbers;
SELECT add_numbers(40, 2);

额外消息:DuckDB Foundation 咨询委员会

秋季起,DuckDB Foundation 会增设利益相关者咨询委员会,为 DuckDB、DuckLake 和 Quack 的开发路线图提供意见,让关键利益相关方对项目方向有发言权。

升级提醒

v2.0 也包含少量破坏性变更:新的默认存储格式、lambda 语法过渡完成等,会在正式发布公告中详细说明。现在就想尝鲜的话,预览版构建已经包含大部分功能。

分享到
微博Twitter

© 2026 四月

原文链接:https://www.aprilzz.com/tutorials/duckdb-2-0-preview