DuckDB 1.3.0 发布
DuckDB 1.3.0 发布
2025.5.21 DuckDB 1.3.0 正式发布, 此版本重构了parquet读写器性能大幅提升; 增加了同时支持字典和fsst压缩的新压缩算法, 大幅提升压缩比; 空间数据支持通过运行时自动对小表构建R树模拟hash_join(对ST_Intersects和ST_Contains等)大幅提升空间JOIN性能;
更多有趣改进请自行发觉;
以下翻译自原文: https://duckdb.org/2025/05/21/announcing-duckdb-130.html
我们非常荣幸地发布了 DuckDB 1.3.0 版本。此版本 DuckDB 被命名为“Ossivalis”,以数百万年前的金眼鸭(Goldeneye duck)的祖先 Bucephala Ossivalis 命名。
在本篇博文中,我们将介绍新版本中最重要的功能。DuckDB 的更新速度很快,我们只能介绍此版本中一小部分的变更。完整的发行说明,请参阅GitHub 上的发行页面。
一、重大变更和弃用
旧版 Linux glibc 弃用
鉴于所有主流 Linux 发行版都使用glibc 2.28或更新版本,DuckDB 的官方 Linux 二进制文件至少需要 glibc 2.28 或更新版本。该版本使用Python manylinux_2_28 镜像构建,该镜像结合了旧版 glibc 和新版编译器。
我们高度重视可移植性,因此当然仍然可以从旧版本的 glibc 的源代码构建 DuckDB。
Lambda 函数语法
以前,DuckDB 中的 lambda 函数可以使用单箭头语法指定:x -> x + 1。单箭头运算符也被 JSON 扩展用来通过 json -> 'field' 语法提取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';
未来计划,DuckDB 1.4.0 启用两种语法样式,即旧的单箭头语法和 Python 风格的语法。DuckDB 1.4.0 将是最后一个支持单箭头语法且未明确启用该语法的版本。DuckDB 1.5.0 将默认禁用单箭头语法。DuckDB 1.6.0 将移除该lambda_syntax标志并完全弃用单箭头语法,因此旧的行为将不再可用。
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 文件代替duckdb数据库文件即可。这将显示一个可供查询的视图。例如,假设我们有一个名为 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,它会尝试强制转换值,但如果无法强制转换,查询也不会失败返回NULL。例如:
SELECT TRY_CAST('asdf' AS INTEGER);
-- return NULL
此版本将此功能扩展至任何可能因使用 而出错的表达式TRY。
例如,0 的对数是 undefined,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 支持
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 拥有一个内部的“secret”管理工具,用于管理 S3 凭证等内容。在此版本中,可以在创建 secret 时使用标量表达式。这使得 secret 内容无需在查询文本中指定,从而更容易避免出现在日志文件等文件中。例如:
SET VARIABLE my_bearer_token = 'hocus pocus this token is bogus';
CREATE SECRET http (
TYPE http,
BEARER_TOKEN getvariable('my_bearer_token')
);
您可以看到,CREATE SECRET 中的secret通过BEARER_TOKEN 字段定义, 是通过getvariable中的函数设置的。在 CLI 中,也可以通过使用 的环境变量getenv()来实现。例如,现在可以这样做:
MY_SECRET=asdf duckdb -c "CREATE SECRET http (TYPE http, BEARER_TOKEN getenv('MY_SECRET'))"
拆columns
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
newUNPACK关键字消除了这个限制。以下表达式
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运算符: 通过R tree “模拟hash join”
spatial我们在扩展中添加了一个新的专门的连接运算符,这大大提高了空间连接的效率,即JOIN使用特定的空间谓词函数(例如ST_Intersects和ST_Contains)查询两个几何列。
与 HASH_JOIN 类似, SPATIAL_JOIN会为连接操作中较小的一侧构建一个临时的查找数据结构,只不过它是 R 树,而不是哈希表。这意味着您无需先创建索引,也无需进行任何其他预处理来优化空间连接。所有这些都由连接运算符内部处理。
虽然查询优化器将尝试为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 中有更多详细信息。
https://github.com/duckdb/duckdb/releases/tag/v1.3.0
三、内部优化
此版本还进行了大量的内部更改。
我们几乎完全重新实现了DuckDB 的Parquet 读写器。这将大大提升 Parquet 的性能和可靠性,并扩展Parquet 对诸如FLOAT16 和 UNKNOWN等较为冷门的逻辑类型的支持。
我们还对名为 MultiFileReader 的 API 中读取多个文件(例如,一个包含 Parquet 文件的文件夹)的功能进行了大量内部改进。我们统一了多种文件读取器(例如 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 上找到。我们要感谢社区提供的详细问题报告和反馈。我们还要特别感谢外部贡献者,他们直接促成了此版本的功能!