宣布 DuckDB 0.10.0

Mark Raasveldt 和 Hannes Mühleisen
2024年2月13日 · 13 分钟阅读

TL;DR: DuckDB 团队很高兴地宣布发布最新版 DuckDB (0.10.0)。此版本以原产于欧洲的绒海鸭命名为 Fusca。

Image of the Velvet Scoter

要安装新版本,请访问安装指南。完整的发布说明可在 GitHub 上找到

0.10.0 版本的新特性

更改内容太多,无法逐一详细讨论,但我们想重点介绍几个特别令人兴奋的特性!

以下是这些新特性及其示例的摘要,首先是我们的 SQL 方言中一项更改,旨在默认情况下生成更直观的结果。

破坏性 SQL 更改

隐式转换为 VARCHAR。以前,DuckDB 在函数绑定期间会自动允许将任何类型隐式转换为 VARCHAR。因此,例如,可以在不使用隐式转换的情况下计算整数的子字符串。从本版本开始,您需要在此处使用显式转换。

SELECT substring(42, 1, 1) AS substr;
No function matches the given name and argument types 'substring(...)'.
You might need to add explicit type casts.

要使用显式转换,请运行

SELECT substring(42::VARCHAR, 1, 1) AS substr;
┌─────────┐
│ substr  │
│ varchar │
├─────────┤
│ 4       │
└─────────┘

或者,可以使用 old_implicit_casting 设置来恢复此行为,例如:

SET old_implicit_casting = true;
SELECT substring(42, 1, 1) AS substr;
┌─────────┐
│ substr  │
│ varchar │
├─────────┤
│ 4       │
└─────────┘

字面量类型。以前,整数和字符串字面量的行为与 INTEGERVARCHAR 类型相同。从本版本开始,INTEGER_LITERALSTRING_LITERAL 是具有自己绑定规则的独立类型。

  • INTEGER_LITERAL 类型可以隐式转换为其值适合的任何整数类型
  • STRING_LITERAL 类型可以隐式转换为任何其他类型

这使得 DuckDB 与 Postgres 对齐,并使字面量上的操作更加直观。例如,我们可以将字符串字面量与日期进行比较——但不能将 VARCHAR 值与日期进行比较。

SELECT d > '1992-01-01' AS result
FROM (VALUES (DATE '1992-01-01')) t(d);
┌─────────┐
│ result  │
│ boolean │
├─────────┤
│ false   │
└─────────┘
SELECT d > '1992-01-01'::VARCHAR AS result
FROM (VALUES (DATE '1992-01-01')) t(d);
Binder Error:
Cannot compare values of type DATE and type VARCHAR – an explicit cast is required

向后兼容性

向后兼容性是指较新的 DuckDB 版本读取由旧版 DuckDB 创建的存储文件的能力。此版本是 DuckDB 首个在存储格式方面支持向后兼容性的版本。DuckDB v0.10 可以读取和操作由旧版 DuckDB (DuckDB v0.9) 创建的文件。这得益于新序列化框架的实现

使用 v0.9 写入

duckdb_092 v092.db
CREATE TABLE lineitem AS
FROM lineitem.parquet;

使用 v0.10 读取

duckdb_0100 v092.db
SELECT l_orderkey, l_partkey, l_comment
FROM lineitem
LIMIT 1;
┌────────────┬───────────┬─────────────────────────┐
│ l_orderkey │ l_partkey │        l_comment        │
│   int32    │   int32   │         varchar         │
├────────────┼───────────┼─────────────────────────┤
│          1 │    155190 │ to beans x-ray carefull │
└────────────┴───────────┴─────────────────────────┘

对于未来的 DuckDB 版本,我们的目标是确保从本版本开始,**之后**发布的任何 DuckDB 版本都可以读取由先前版本创建的文件。我们希望确保文件格式完全向后兼容。这使您可以保留存储在 DuckDB 文件中的数据,并保证您无需担心文件是用哪个版本写入的或无需在版本之间转换文件即可读取文件。

向前兼容性

向前兼容性是指旧版 DuckDB 读取由新版 DuckDB 生成的存储文件的能力。DuckDB v0.9 与 DuckDB v0.10 **部分**向前兼容。由 DuckDB v0.10 创建的某些文件可以被 DuckDB v0.9 读取。

使用 v0.10 写入

duckdb_0100 v010.db
CREATE TABLE lineitem AS
FROM lineitem.parquet;

使用 v0.9 读取

duckdb_092 v010.db
SELECT l_orderkey, l_partkey, l_comment
FROM lineitem
LIMIT 1;
┌────────────┬───────────┬─────────────────────────┐
│ l_orderkey │ l_partkey │        l_comment        │
│   int32    │   int32   │         varchar         │
├────────────┼───────────┼─────────────────────────┤
│          1 │    155190 │ to beans x-ray carefull │
└────────────┴───────────┴─────────────────────────┘

向前兼容性是**尽力而为**提供的。虽然存储格式的稳定性很重要,但未来我们仍希望对存储格式进行许多改进和创新。因此,向前兼容性有时可能会(部分)被破坏。

对于本版本,DuckDB v0.9 能够读取由 DuckDB v0.10 创建的文件,前提是:

  • 数据库文件不包含视图
  • 数据库文件不包含新类型(ARRAY, UHUGEINT
  • 数据库文件不包含索引(PRIMARY KEY, FOREIGN KEY, UNIQUE, 显式索引)
  • 数据库文件不包含新的压缩方法(ALP)。由于 ALP 会自动用于压缩 FLOATDOUBLE 列,这意味着在实践中,除非通过配置明确禁用 ALP,否则向前兼容性通常不适用于 FLOATDOUBLE 列。

我们预计随着格式的稳定和成熟,这种情况会越来越少发生——我们希望提供更好的保证,使 DuckDB 能够读取由未来 DuckDB 版本写入的文件。

CSV 读取器重构

CSV 读取器重构 CSV 读取器在此版本中进行了重大改进。新的 CSV 读取器使用高效的状态机转换来快速处理 CSV 文件。这大大加快了 CSV 读取器的性能,尤其是在多线程场景中。此外,对于格式错误的 CSV 文件,报告的错误消息应更清晰。

以下是在一台 M1 Max (10 核) 上从 CSV 文件加载 1100 万行 NYC 出租车数据集的基准测试,比较了加载时间

版本 加载时间
v0.9.2 2.6 秒
v0.10.0 1.2 秒

此外,还进行了许多优化,使得直接对 CSV 文件运行查询的速度也大大加快。以下是直接对 NYC 出租车 CSV 文件执行 SELECT count(*) 查询的执行时间比较基准测试。

版本 查询时间
v0.9.2 1.8 秒
v0.10.0 0.3 秒

定长数组

定长数组 本版本引入了定长数组类型。定长数组类似于列表,但其中每个值都必须包含相同数量的固定元素。

CREATE TABLE vectors (v DOUBLE[3]);
INSERT INTO vectors VALUES ([1, 2, 3]);

定长数组比变长列表操作更快,因为每个列表元素的大小是预先知道的。本版本还引入了对这些数组进行操作的专用函数,例如 array_cross_productarray_cosine_similarityarray_inner_product

SELECT array_cross_product(v, [1, 1, 1]) AS result
FROM vectors;
┌───────────────────┐
│      result       │
│     double[3]     │
├───────────────────┤
│ [-1.0, 2.0, -1.0] │
└───────────────────┘

有关更多信息,请参阅文档中的数组类型页面

多数据库支持

DuckDB 现在除了自身格式存储的数据库外,还可以附加 MySQL、Postgres 和 SQLite 数据库。这使得数据能够方便地读取到 DuckDB 中并在这些系统之间移动,因为附加的数据库功能齐全,表现得像常规表一样,并且可以以安全、事务性的方式进行更新。有关多数据库支持的更多信息,请参阅我们最近的博客文章

ATTACH 'sqlite:sakila.db' AS sqlite;
ATTACH 'postgres:dbname=postgresscanner' AS postgres;
ATTACH 'mysql:user=root database=mysqlscanner' AS mysql;

密钥管理器

DuckDB 集成了 S3 等多个云存储系统,这些系统需要访问凭据才能访问数据。在当前版本的 DuckDB 中,身份验证信息通过 DuckDB 设置进行配置,例如 SET s3_access_key_id = '...';。尽管这可行,但它有一些缺点。例如,如果不修改查询之间的设置,就不可能为不同的 S3 存储桶设置不同的凭据。由于设置不被视为秘密,因此也可以使用 duckdb_settings() 查询它们。

在此版本中,DuckDB 添加了一个新的“密钥管理器”以更好地管理密钥。我们现在为所有使用密钥的后端提供了统一的密钥用户界面。密钥可以设置作用域,因此不同的存储前缀可以拥有不同的密钥,例如,允许在单个查询中进行跨组织连接。密钥也可以持久化,因此无需在每次启动 DuckDB 时都指定它们。

密钥是类型化的,其类型标识它们所针对的服务。例如,此版本可以管理 S3、Google Cloud Storage、Cloudflare R2 和 Azure Blob Storage 的密钥。对于每种类型,都有一个或多个“密钥提供程序”,它们指定如何创建密钥。密钥还可以具有可选的作用域,即密钥适用的文件路径前缀。当为某个路径获取密钥时,会将密钥作用域与该路径进行比较,返回与该路径匹配的密钥。如果存在多个匹配的密钥,则选择最长的前缀。

最后,密钥可以是临时的或持久的。默认情况下使用临时密钥——它们在 DuckDB 实例的生命周期内存储在内存中,类似于以前设置的工作方式。持久密钥以未加密的二进制格式存储在 ~/.duckdb/stored_secrets 目录中。在 DuckDB 启动时,持久密钥会从该目录中读取并自动加载。

例如,要创建一个临时的无作用域密钥来访问 S3,我们现在可以使用以下语法

CREATE SECRET (
    TYPE s3,
    KEY_ID 'AKIAIOSFODNN7EXAMPLE',
    SECRET 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
    REGION 'us-east-1'
);

如果一个服务类型存在两个密钥,可以使用作用域来决定应使用哪个。例如

CREATE SECRET secret1 (
    TYPE s3,
    KEY_ID 'my_key1',
    SECRET 'my_secret1',
    SCOPE 's3://my-bucket'
);

CREATE SECRET secret2 (
    TYPE s3,
    KEY_ID 'my_key2',
    SECRET 'my_secret2',
    SCOPE 's3://my-other-bucket'
);

现在,如果用户从 s3://my-other-bucket/something 查询某些内容,该请求将自动选择 secret2

可以使用内置的表生成函数列出密钥,例如,通过使用 FROM duckdb_secrets();。敏感信息将被隐藏。

为了在 DuckDB 数据库实例之间持久化密钥,我们现在可以使用 CREATE PERSISTENT SECRET 命令,例如:

CREATE PERSISTENT SECRET my_persistent_secret (
    TYPE s3,
    KEY_ID 'my_key',
    SECRET 'my_secret'
);

如前所述,这会将密钥(未加密,请注意)写入 ~/.duckdb/stored_secrets 目录。

有关更多信息,请参阅文档中的创建密钥页面

临时内存管理器

DuckDB 支持超内存操作,这意味着像聚合和连接这样内存密集型的操作符,在内存不足时可以将部分中间结果卸载到磁盘上的临时文件中。

以前,如果这些操作符的内存使用量达到可用内存(由内存限制定义)的约 60%,它们就会开始卸载到磁盘。如果同时只发生其中一个操作,这会运行良好。如果多个内存密集型操作同时发生,它们的组合内存使用量可能会超出内存限制,导致 DuckDB 抛出错误。

本版本引入了所谓的“临时内存管理器”,它管理并发操作的临时内存。其工作方式如下:内存密集型操作向临时管理器注册。管理器根据线程数和当前内存限制,为每个注册的操作保证一定的最小内存量。然后,内存密集型操作会告知它们当前希望使用多少内存。管理器可以批准此请求或响应减少的分配量。在分配量减少的情况下,操作符将需要动态地降低其内存需求,例如通过切换算法。

例如,如果可用内存不足,哈希连接可能会调整其操作,执行分区哈希连接而不是完整的内存内连接。

下面是一个示例

PRAGMA memory_limit = '5GB';
SET temp_directory = '/tmp/duckdb_temporary_memory_manager';

CREATE TABLE tbl AS
SELECT range AS i,
       range AS j
FROM range(100_000_000);

SELECT max(i),
       max(t1.j),
       max(t2.j),
       max(t3.j),
FROM tbl AS t1
JOIN tbl AS t2 USING (i)
JOIN tbl AS t3 USING (i);

请注意,这里必须设置一个临时目录,因为在给定此内存限制的情况下,操作符实际上需要将数据卸载到磁盘才能完成此查询。

使用新版本 0.10.0,此查询在 MacBook 上大约 5 秒内完成,而它在先前版本中会因 Error: Out of Memory Error: failed to pin block of size ... 错误而失败。

自适应无损浮点压缩 (ALP)

浮点数在压缩比以及压缩和解压缩速度方面都难以有效压缩。过去,DuckDB 支持当时最先进的“Chimp”和“Patas”压缩方法。事实证明,这些并不是浮点压缩的终极方案。研究人员 Azim AfroozehLeonard Kuffo 和(独一无二的)Peter Boncz 最近在 SIGMOD(一个数据管理研究的顶级学术会议)上发表了一篇题为“ALP:自适应无损浮点压缩”的论文。他们还采取了一项不同寻常但值得高度赞扬的举动,向 DuckDB 发送了一个拉取请求。新的压缩方案取代了 Chimp 和 Patas。在 DuckDB 内部,ALP 比 Patas 快 2-4 倍(在解压缩时),并且实现了两倍的压缩比(有时甚至更高)。

压缩 加载 查询 大小
ALP 0.434 秒 0.020 秒 184 MB
Patas 0.603 秒 0.080 秒 275 MB
未压缩 0.316 秒 0.012 秒 489 MB

作为用户,您无需做任何事情即可使用新的 ALP 压缩方法,DuckDB 将在检查点期间自动决定使用 ALP 是否对特定数据集有益。

CLI 改进

命令行客户端在此版本中进行了大量工作。特别是,多行编辑已成为默认模式,并进行了许多改进。查询历史现在也是多行的。语法高亮已改进——缺失的括号和未闭合的引号会以错误突出显示,并且当光标移动到匹配的括号上时,它们也会被高亮显示。与 read-line 的兼容性也得到了大大扩展

Image showing syntax highlighting in the shell

有关更多信息,请参阅扩展的 CLI 文档

结语

以上只是几个亮点——但此版本中还有许多其他特性和改进。下面是一些更多的亮点。完整的发布说明可在 GitHub 上找到

新特性

新函数

存储改进

优化

致谢

我们要感谢所有贡献者为改进 DuckDB 所做的辛勤工作。