发布 DuckDB 1.3.0
TL;DR: DuckDB 团队很高兴地宣布,我们今天发布了代号为“Ossivalis”的 DuckDB 1.3.0 版本。
要安装新版本,请访问安装指南。请注意,由于需要额外的更改和审核轮次,一些客户端库(例如 Go、R、Java)和扩展(例如 UI)的发布可能需要数小时到数天的时间。
我们很荣幸发布 DuckDB 1.3.0。此版本的 DuckDB 命名为“Ossivalis”,源自 Bucephala Ossivalis,这是一种数百万年前生活着的斑头秋沙鸭的祖先。
在这篇博客文章中,我们介绍了新版本最重要的功能。DuckDB 发展迅速,我们只能介绍此次发布中一小部分更改。有关完整的发布说明,请参阅 GitHub 上的发布页面。
重大变更和弃用
旧版 Linux glibc 弃用
鉴于所有主流 Linux 发行版都使用 glibc 2.28 或更高版本,DuckDB 的官方 Linux 二进制文件现在要求至少 glibc 2.28 或更高版本。此版本是使用 Python 的 manylinux_2_28
镜像构建的,该镜像结合了旧版 glibc 和新版编译器。此更改也意味着扩展不再针对 linux_amd64_gcc4
平台分发。
我们高度重视可移植性,因此当然仍有可能为旧版本的 glibc 从源代码构建 DuckDB。
Lambda 函数语法
以前,DuckDB 中的 lambda 函数可以使用单箭头语法指定:x -> x + 1
。JSON 扩展也使用单箭头运算符来表示JSON 提取,语法为 ->'field'
。单箭头运算符的两种含义在绑定器中被同等对待,因此它们共享相同的(低)优先级,这使得在包含等式检查的 JSON 表达式中需要额外的括号。
SELECT (JSON '{"field": 42}')->'field' = 42;
-- throws a Binder Error:
-- No function matches the given name and argument types 'json_extract(JSON, BOOLEAN)
SELECT ((JSON '{"field": 42}')->'field') = 42;
-- return true
这通常导致用户混淆,因此新版本弃用了旧的箭头 lambda 语法,并将其替换为 Python 风格的 lambda 语法。
SELECT list_transform([1, 2, 3], lambda x: x + 1);
为了使过渡更顺畅,弃用将在未来一年内分几个步骤进行。首先,DuckDB 1.3.0 引入了一个新设置来配置 lambda 语法。
SET lambda_syntax = 'DEFAULT';
SET lambda_syntax = 'ENABLE_SINGLE_ARROW';
SET lambda_syntax = 'DISABLE_SINGLE_ARROW';
目前,DEFAULT
启用两种语法样式,即旧的单箭头语法和 Python 风格的语法。DuckDB 1.4.0 将是最后一个在不显式启用情况下支持单箭头语法的版本。DuckDB 1.5.0 将默认禁用单箭头语法。DuckDB 1.6.0 将移除 lambda_syntax
标志并完全弃用单箭头语法,因此旧行为将不再可用。
带转义符的列表字符串序列化
从新版本开始,DuckDB 在嵌套数据结构中序列化的字符串中转义诸如 '
等字符,以允许序列化字符串和嵌套表示之间进行往返转换。例如
SELECT ['hello ''my'' world'] AS s;
DuckDB 1.2.2 版本返回 [hello 'my' world]
,而 DuckDB 1.3.0 返回 ['hello \'my\' world']
。
要使用旧行为序列化字符串列表,请使用 array_to_string
函数。
SELECT printf('[%s]', array_to_string(
['hello ''my'' world', 'hello ''cruel'' world'], ', '
)) AS s;
┌─────────────────────────────────────────┐
│ s │
│ varchar │
├─────────────────────────────────────────┤
│ [hello 'my' world, hello 'cruel' world] │
└─────────────────────────────────────────┘
SQL 解析器次要更改
- 术语
AT
现在需要引号才能用作标识符,因为它用于 Iceberg 中的时间旅行。 - 由于 lambda 语法的更改,
LAMBDA
现在是保留关键字。 GRANT
不再是保留关键字。
新功能
新版 DuckDB 再次包含许多令人兴奋的新功能。
外部文件缓存
DuckDB 经常用于读取远程文件,例如存储在 HTTP 服务器或 blob 存储上的 Parquet 文件。以前的版本总是完全重新读取文件数据。在此版本中,我们为外部文件数据添加了缓存。此缓存受 DuckDB 整体内存限制。如果有可用空间,它将用于动态缓存外部文件数据。这应该会大大改善在远程数据上重新运行查询的性能。例如
.timer on
.mode trash -- do not show query result
FROM 's3://duckdb-blobs/data/shakespeare.parquet';
Run Time (s): real 1.456 user 0.037920 sys 0.028510
FROM 's3://duckdb-blobs/data/shakespeare.parquet';
Run Time (s): real 0.360 user 0.029188 sys 0.007620
我们可以看到,由于缓存的存在,第二次查询速度快了很多。在以前的版本中,运行时会相同。
缓存内容可以使用 duckdb_external_file_cache()
表函数进行查询,如下所示
.mode duckbox -- re-enable output
FROM duckdb_external_file_cache();
┌────────────────────────────────────────────┬──────────┬──────────┬─────────┐
│ path │ nr_bytes │ location │ loaded │
│ varchar │ int64 │ int64 │ boolean │
├────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ s3://duckdb-blobs/data/shakespeare.parquet │ 1697483 │ 4 │ true │
│ s3://duckdb-blobs/data/shakespeare.parquet │ 16384 │ 1681808 │ true │
└────────────────────────────────────────────┴──────────┴──────────┴─────────┘
缓存默认启用,但可以通过以下方式禁用:
SET enable_external_file_cache = false;
使用 CLI 直接查询数据文件
DuckDB 的命令行界面 (CLI) 获得了直接查询 Parquet、CSV 或 JSON 文件的能力。这只需使用例如 Parquet 文件而不是数据库文件即可实现。这将暴露一个可供查询的视图。例如,假设我们有一个名为 region.parquet
的 Parquet 文件,这将有效:
duckdb region.parquet -c 'FROM region;'
┌─────────────┐
│ r_name │
│ varchar │
├─────────────┤
│ AFRICA │
│ AMERICA │
│ ASIA │
│ EUROPE │
│ MIDDLE EAST │
└─────────────┘
当像这样使用 CLI 时,实际发生的是我们启动了一个临时的内存中 DuckDB 数据库,并为给定文件创建了两个视图:
file
– 此视图始终同名,无论文件名称如何。[base_file_name]
– 此视图取决于文件名称,例如对于region.parquet
,它就是region
。
这两个视图都可以查询,并会给出相同的结果。
此功能的主要优势在于可用性:我们可以使用常规 shell 导航到文件,然后使用 DuckDB 打开该文件,而无需在 SQL 级别引用文件路径。
TRY
表达式
DuckDB 已经支持 TRY_CAST
,它会尝试转换一个值,如果不可能也不会使查询失败。例如
SELECT TRY_CAST('asdf' AS INTEGER);
返回 NULL
。此版本将此功能泛化到超出类型转换的任意可能出错的表达式,使用 TRY
。例如,0 的对数是未定义的,并且 log(0)
将抛出异常并提示“无法计算零的对数”。使用新的 TRY
,这将返回 NULL
,例如
SELECT TRY(log(0));
NULL
同样,这将适用于任意表达式。但是,如果经常预期会出现错误,我们建议谨慎使用 TRY
,因为它会带来性能影响。如果任何批次的行导致错误,我们将切换到逐行执行表达式,以准确找出哪些行有错误,哪些没有。这会更慢。
更新结构体
从新版本开始,可以使用 ALTER TABLE
子句更新结构体的子模式。您可以添加、删除和重命名字段。
CREATE TABLE test(s STRUCT(i INTEGER, j INTEGER));
INSERT INTO test VALUES (ROW(1, 1)), (ROW(2, 2));
ALTER TABLE test DROP COLUMN s.i;
ALTER TABLE test ADD COLUMN s.k INTEGER;
ALTER TABLE test RENAME COLUMN s.j TO l;
┌──────────────────────────────┐
│ s │
│ struct(l integer, k integer) │
├──────────────────────────────┤
│ {'l': 1, 'k': NULL} │
│ {'l': 2, 'k': NULL} │
└──────────────────────────────┘
更改结构体也支持在 LIST
和 MAP
列中。
交换新数据库
ATTACH OR REPLACE
子句允许您替换数据库,从而可以即时交换数据库。例如
ATTACH 'taxi_v1.duckdb' AS taxi;
USE taxi;
ATTACH OR REPLACE 'taxi_v2.duckdb' AS taxi;
此功能由外部贡献者 xevix
实现。
UUID v7 支持
警告 更新 (2025-05-23)。 DuckDB v1.3.0 中的 UUID v7 实现与 UUID 标准不一致,导致时间戳值不准确。这意味着生成的 UUID v7 值中的时间戳只有在 DuckDB 内部独占使用时才是正确的。从其他系统导入或导出 UUID v7 值将产生不正确的时间戳。我们已修复此错误,修复程序将很快在预览版本和即将发布的 1.3.1 补丁版本中提供。
DuckDB 现在支持 UUID v7,这是 UUID 的一个较新版本。UUIDv7
结合了毫秒级的 Unix 时间戳和随机位,提供了唯一性和可排序性。这对于例如按年龄排序 UUID 或将许多表中普遍存在的 ID
和 TIMESTAMP
列组合成一个 UUIDv7
列非常有用。
可以使用 uuidv7()
标量函数创建新的 UUID。例如
SELECT uuidv7();
┌──────────────────────────────────────┐
│ uuidv7() │
│ uuid │
├──────────────────────────────────────┤
│ 8196f1f6-e3cf-7a74-bc0e-c89ac1ea1e19 │
└──────────────────────────────────────┘
还有其他函数用于确定 UUID 版本 (uuid_extract_version()
) 和提取内部时间戳 (uuid_extract_timestamp()
),例如
SELECT uuid_extract_version(uuidv7());
┌────────────────────────────────┐
│ uuid_extract_version(uuidv7()) │
│ uint32 │
├────────────────────────────────┤
│ 7 │
└────────────────────────────────┘
SELECT uuid_extract_timestamp(uuidv7());
┌──────────────────────────────────┐
│ uuid_extract_timestamp(uuidv7()) │
│ timestamp with time zone │
├──────────────────────────────────┤
│ 2025-05-21 08:32:14.61+00 │
└──────────────────────────────────┘
此功能由外部贡献者 dentiny
实现。
CREATE SECRET
中的表达式支持
DuckDB 有一个内部“密钥”管理工具,用于 S3 凭据等。在此版本中,可以在创建密钥时使用标量表达式。这使得密钥内容无需在查询文本中指定,从而更易于避免将其写入日志文件等。例如
SET VARIABLE my_bearer_token = 'hocus pocus this token is bogus';
CREATE SECRET http (
TYPE http,
BEARER_TOKEN getvariable('my_bearer_token')
);
您可以看到密钥中的 BEARER_TOKEN
字段是从 CREATE SECRET
中的 getvariable
函数设置的。在 CLI 中,这也可以通过使用 getenv()
的环境变量来实现。例如,现在可以这样做了:
MY_SECRET=asdf duckdb -c \
"CREATE SECRET http (TYPE http, BEARER_TOKEN getenv('MY_SECRET'))"
解包列
DuckDB v1.3.0 进一步提升了流行的 COLUMNS(*)
表达式。以前,通过添加前导 *
字符可以将实体解包到列表中。
CREATE TABLE tbl AS SELECT 21 AS a, 1.234 AS b;
SELECT [*COLUMNS(*)] AS col_exp FROM tbl;
┌─────────────────┐
│ col_exp │
│ decimal(13,3)[] │
├─────────────────┤
│ [21.000, 1.234] │
└─────────────────┘
然而,此语法不能与其他表达式(例如类型转换)结合使用。
SELECT [*COLUMNS(*)::VARCHAR] AS col_exp FROM tbl;
Binder Error:
*COLUMNS() can not be used in this place
新的 UNPACK
关键字消除了这一限制。以下表达式
SELECT [UNPACK(COLUMNS(*)::VARCHAR)] AS col_exp FROM tbl;
等价于
SELECT [a::VARCHAR, b::VARCHAR] AS col_exp FROM tbl;
┌─────────────┐
│ col_exp │
│ varchar[] │
├─────────────┤
│ [21, 1.234] │
└─────────────┘
空间 JOIN
运算符
作为 spatial
扩展的一部分,我们添加了一个新的专用连接运算符,它大大提高了空间连接的效率,即使用特定的空间谓词函数(例如 ST_Intersects
和 ST_Contains
)连接两个几何列的查询。
与 HASH_JOIN
类似,SPATIAL_JOIN
为连接的较小一侧构建一个临时查找数据结构,但它是一个 R-Tree,而不是哈希表。这对您意味着您无需先创建索引或进行任何其他预处理来优化空间连接。这一切都由连接运算符在内部处理。
虽然查询优化器会尝试为 LEFT
、OUTER
、INNER
和 RIGHT
空间连接实例化此新运算符,但目前的一个限制是连接只能包含一个连接条件,否则优化器将回退到使用效率较低的连接策略。
以下示例说明了 SPATIAL_JOIN
运算符如何成为查询计划的一部分。这是一个相对较小的查询,但在我的机器上,它的执行速度几乎比 DuckDB v1.2.2 中快 100 倍!
LOAD spatial;
-- generate random points
CREATE TABLE points AS
SELECT
ST_Point(x, y) AS geom,
(y * 50) + x // 10 AS id
FROM
generate_series(0, 1000, 5) r1(x),
generate_series(0, 1000, 5) r2(y);
-- generate random polygons
CREATE TABLE polygons AS
SELECT
ST_Buffer(ST_Point(x, y), 5) AS geom,
(y * 50) + x // 10 AS id
FROM
generate_series(0, 500, 10) r1(x),
generate_series(0, 500, 10) r2(y);
-- inspect the join plan
EXPLAIN
SELECT *
FROM polygons
JOIN points ON ST_Intersects(points.geom, polygons.geom);
...
┌─────────────┴─────────────┐
│ SPATIAL_JOIN │
│ ──────────────────── │
│ Join Type: INNER │
│ Conditions: ├──────────────┐
│ ST_Intersects(geom, geom) │ │
│ ~40401 Rows │ │
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ Table: points ││ Table: polygons │
│ Type: Sequential Scan ││ Type: Sequential Scan │
│ ~40401 Rows ││ ~2601 Rows │
└───────────────────────────┘└───────────────────────────┘
感兴趣的读者可以在此 PR 中找到更多详细信息。
内部更改
本次发布还有大量内部更改。
我们已经几乎完全重新实现了 DuckDB 的 Parquet 读取器和写入器。这应该会大大提高 Parquet 的性能和可靠性,并且还扩展了对 Parquet 功能的支持,包括 UNKNOWN
和 FLOAT16
等不常见的逻辑类型。
我们还在 多文件读取 方面进行了大量内部更改(例如,一个包含 Parquet 文件的文件夹),使用名为 MultiFileReader
的 API。我们统一了许多文件读取器(例如 Parquet、CSV、JSON、Avro 等)中多个文件的处理方式。这使得 DuckDB 能够以统一的方式处理例如多个文件之间的模式差异。
我们还添加了一种新的字符串压缩方法:DICT_FSST
。此前,DuckDB 支持字符串的字典编码或FSST 压缩(“快速静态符号表”)。这些压缩方法不能在存储块内混合使用(默认 265 KB)。然而,我们观察到许多实际数据中,部分块会从字典编码中受益,而另一部分则会从 FSST 中受益。FSST 默认不会消除字符串重复。此版本将这两种方法结合起来,形成了一种新的压缩方法:DICT_FFST
。它首先运行字典编码,然后使用 FSST 压缩字典。字典编码和纯 FSST 编码仍然可用。我们还在本次发布中优化了有效性掩码的存储(“哪些行是 NULL?”),一些压缩方法(如新的 DICT_FSST
)可以在内部处理 NULL 值,从而无需单独的有效性掩码。总的来说,这些新功能应该会大大减少所需的存储空间,特别是对于字符串。请注意,压缩方法是 DuckDB 根据实际观察到的压缩率自动选择的,因此用户无需显式设置。
结语
以上只是一些亮点——但此版本还有更多功能和改进。自 v1.2.2 发布以来,已有超过 75 位贡献者提交了 3,000 多次提交。完整的发布说明可在 GitHub 上找到。我们要感谢社区提供了详细的问题报告和反馈。特别感谢外部贡献者,他们直接在此版本中贡献了功能!