DuckDB Avro 扩展

Author Avatar
Hannes Mühleisen
2024-12-09 · 9 分钟

概要:DuckDB 现在支持通过 avro 社区扩展读取 Avro 文件。

更新(2025 年 5 月): Avro 支持现在已作为 avro 核心扩展提供。

Apache™ Avro™ 格式

Avro 是一种记录数据的二进制格式。 像数据领域的许多创新一样,Avro 由 Doug Cutting 开发,作为 Apache Hadoop 项目的一部分,大约在 2009 年。 Avro 的名字有点晦涩,来源于一家倒闭的 英国飞机制造商。 该公司在第二次世界大战的严峻条件下,建造了超过 7,000 架 Avro Lancaster 重型轰炸机。 但我们跑题了。

Avro 格式是又一次尝试解决将复杂的多维数据结构(如表(可能具有嵌套类型))转换为一维存储布局(如平面文件,它只是一个字节序列)时出现的降维问题。 这里出现的最根本问题是使用列式布局还是行式布局。 Avro 使用行式布局,这使其与其著名的表兄弟 Apache™ Parquet™ 格式区分开来。 行式格式有其有效的使用场景:例如,由于 Parquet 的列式布局以及 Parquet 元数据存储在文件末尾的事实,因此向 Parquet 文件附加少量行很困难且效率低下。 在像 Avro 这样的行式格式中,元数据位于顶部,我们可以“只是”将这些行添加到文件的末尾,就完成了。 这使 Avro 能够相当有效地处理少量行的附加。

Avro 编码的数据可以以多种方式出现,例如在 RPC 消息中,也可以在文件中。 在下文中,我们重点关注文件,因为这些文件可以长期保存。

头部块

Avro “对象容器”文件使用相对简单的二进制 格式进行编码:每个文件都以一个头部块开头,该头部块首先具有 魔术字节 Obj1。 然后,是一个元数据“映射”(字符串-字节数组键值对的列表)。 该映射严格要求仅包含一个用于 avro.schema 键的条目。 此键包含编码为 JSON 的 Avro 文件模式。 这是一个此类模式的示例

{
  "namespace": "example.avro",
  "type": "record",
  "name": "User",
  "fields": [
     {"name": "name", "type": "string"},
     {"name": "favorite_number", "type": ["int", "null"]},
     {"name": "favorite_color", "type": ["string", "null"]}
  ]
}

Avro 模式定义了记录结构。 记录可以包含标量数据字段(如 intdoublestring 等),以及更复杂的类型,如记录(类似于 DuckDB STRUCT)、联合和列表。 顺便说一句,用于定义记录结构的数据格式会回退到像 JSON 这样的另一种格式来描述自己,这很奇怪,但这就是 Avro 的怪异之处。

数据块

头部以 16 个随机选择的字节作为“同步标记”结束。 头部之后是任意数量的数据块:每个数据块都以记录计数开始,后跟大小和包含实际记录的字节数组。 可选地,可以使用 deflate (gzip) 压缩字节,这将从头部元数据中得知。

只能使用模式解码数据字节。 对象文件规范包含有关如何编码每种类型的详细信息。 例如,在示例模式中,我们知道每个值都是具有三个字段的记录。 根级别的记录将按照声明的顺序编码其条目。 不需要为此使用任何实际字节。 首先,我们将读取 name 字段。 字符串由长度后跟字符串字节组成。 与其他格式(例如,Thrift)一样,Avro 使用 使用 zigzag 编码的可变长度整数来存储长度和计数等。 读取字符串后,我们可以继续 favorite_number。 此字段是联合类型(使用 [] 语法编码)。 此联合可以具有两种类型的值,intnullnull 类型有点奇怪,它只能用于编码值缺失的事实。 为了解码 favorite_number 字段,我们首先读取一个 int,它编码了使用联合的哪个选择。 之后,我们使用“正常”解码器读取值(例如,intnull)。 对于 favorite_color,也可以这样做。 每个数据块再次以同步标记结束。 同步标记可用于验证块是否已完全写入以及文件中没有垃圾。

DuckDB avro 社区扩展

我们开发了一个 DuckDB 社区扩展,使 DuckDB 能够读取 Apache Avro™ 文件。

该扩展不包含 Avro 写入功能。 这是故意的,通过不提供写入器,我们希望随着时间的推移减少世界上 Avro 文件的数量。

安装和加载

通过 DuckDB 社区扩展存储库可以轻松安装,只需键入

INSTALL avro FROM community;
LOAD avro;

在你附近的 DuckDB 实例中。

自 DuckDB v1.2.1 起,也支持 DuckDB 的 WebAssembly 客户端。

read_avro 函数

该扩展添加了一个 DuckDB 函数 read_avro。 可以像这样使用此函数

FROM read_avro('some_example_file.avro');

此函数会将 Avro 文件的内容公开为 DuckDB 表。 然后,您可以使用任何任意 SQL 构造来进一步转换此表。

文件 IO

read_avro 函数已集成到 DuckDB 的文件系统抽象中,这意味着您可以直接从 HTTP 或 S3 源读取 Avro 文件。 例如

FROM read_avro('http://blobs.duckdb.org/data/userdata1.avro');
FROM read_avro('s3://my-example-bucket/some_example_file.avro');

应该“只是”工作。

您还可以在单个读取调用中 glob 多个文件,或者将文件列表传递给函数

FROM read_avro('some-example-file-*.avro');
FROM read_avro(['some-example-file-1.avro', 'some-example-file-2.avro']);

如果文件名以某种方式包含有价值的信息(不幸的是,这种情况非常普遍),您可以将 filename 参数传递给 read_avro

FROM read_avro('some-example-file-*.avro', filename = true);

这将在结果集中产生一个额外的列,其中包含 Avro 文件的实际文件名。

模式转换

此扩展自动将 Avro 模式转换为 DuckDB 模式。 可以转换所有 Avro 类型,但递归类型定义除外,DuckDB 不支持。

类型映射非常简单,除了 Avro 处理 NULL 的“独特”方式。 与其他系统不同,Avro 不会将 NULL 视为例如 INTEGER 范围内的可能值,而是将 NULL 表示为实际类型与特殊 NULL 类型的联合。 这与 DuckDB 不同,在 DuckDB 中,任何值都可以为 NULL。 当然,DuckDB 也支持 UNION 类型,但这使用起来会很麻烦。

此扩展在可能的情况下简化 Avro 模式:任何类型与特殊空类型的 Avro 联合都简化为非空类型。 例如,类型为 ["int", "null"] 的 Avro 记录(例如 示例中的 favorite_number)变为 DuckDB INTEGER,它只是有时为 NULL。 类似地,仅包含单个类型的 Avro 联合会转换为它包含的类型。 例如,类型为 ["int"] 的 Avro 记录也变为 DuckDB INTEGER

该扩展还“扁平化”了 Avro 模式。 Avro 将表定义为根级别的“记录”字段,这些字段与 DuckDB STRUCT 字段相同。 为了更方便地处理,此扩展将单个顶级记录的条目转换为顶级列。

实现

在内部,此扩展使用“官方”的 Apache Avro C API,但进行了一些小的修补,以允许从内存中读取 Avro 文件。

局限性和后续步骤

在下文中,我们将披露 avro DuckDB 扩展的局限性,以及我们未来减轻这些局限性的计划

  • 该扩展当前在读取单个(大型)Avro 文件或读取文件列表时未使用并行性。 在后一种情况下添加对并行性的支持已在路线图上。

  • 目前没有对投影或过滤器下推的支持,但这也在稍后的阶段计划中。

  • 如上所述,DuckDB 无法表达 Avro 具有的递归类型定义。 这不太可能发生变化。

  • 不支持允许用户提供单独的 Avro 模式文件。 这不太可能改变,到目前为止,我们看到的所有 Avro 文件都嵌入了它们的模式。

  • 目前不支持 DuckDB 中其他读取器支持的 union_by_name 标志。 这是未来计划的。

结论

DuckDB 的新 avro 社区扩展使 DuckDB 能够直接读取 Avro 文件,就像它们是表一样。 如果你有一堆 Avro 文件,那就去尝试一下吧! 如果您遇到任何问题,我们很乐意收到您的 来信