PostgreSQL码农集散地

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 上找到。我们要感谢社区提供的详细问题报告和反馈。我们还要特别感谢外部贡献者,他们直接促成了此版本的功能!