生成式 AI 正在改变数据湖里“数据”的含义,以前我们面对的数据都是数值、字符串、日期或嵌套结构;如今多模态训练、文档理解、语音分析等工作负载,要求数据湖同时管理图片、音频、视频、PDF 以及模型文件。这些对象通常体积较大,存储在对象存储中,但又需要和标签、业务属性、数据版本、训练快照一起被查询和治理。
过去,工程团队通常只有两种选择:
- 把内容作为
BINARY直接写进 Parquet。 - 在 Parquet 中保存一个指向对象存储的字符串路径。
第一种方式容易让大对象进入 Parquet 的 Page 和 Row Group,会影响扫描、内存占用和文件布局。AI 时代,数据库需要具备什么样的能力?这篇文章有具体提到怎么个影响法,这里不过多阐述。
第二种方式虽然轻量,却没有标准语义——Reader 不知道字符串是不是文件、是否只引用其中一段、如何判断内容类型、如何校验内容。总的来说就是没有个标准,各个业务团队各自定义各自的语义。
Apache Parquet 的新 FILE Logical Type,试图解决的正是这个问题。
它的价值不是简单地“把文件塞进 Parquet”,而是让 Parquet 生态第一次拥有一种统一、可互操作的非结构化字节对象语义:内容可以直接内嵌,也可以放在当前 Parquet 文件的其他位置,还可以位于外部对象存储;对上层系统而言,它们都是同一种 FILE。
这一标准目前已经在 PR 585 里面被正式合入了。不过规范刚落地,目前只有 parquet-java(PR 3608)和 arrow-rs(PR 10109)的 PoC 实现,各查询引擎的支持还需要时间。
FILE 不是一种新的物理编码
Parquet 很聪明,没有增加新的 encoding 格式,而是选择直接复用了 STRUCT 能力,然后在这个 STRUCT group 之上,标注了 FILE 逻辑类型。这样大大提升了兼容性,且降低了现有引擎的适配难度。
当前规范中的完整定义如下:
optional group my_file (FILE) {
optional binary uri (STRING);
optional int64 offset;
optional int64 size;
optional binary content_type (STRING);
optional binary checksum (STRING);
optional binary inline;
}
从物理视角看,它仍然只是六个普通叶子列:
my_file
├── uri
├── offset
├── size
├── content_type
├── checksum
└── inline
FILE annotation 告诉 Reader:这些字段不是任意业务 Struct,而是共同描述一段可解析的字节。
所有字段都是 optional,Writer 不必定义自己永远不会使用的字段。例如,一个只保存外部对象引用的 Schema 可以只有 uri、content_type 和 checksum;一个只内嵌小对象的 Schema,可以只有 inline 和 content_type。
但“字段都是 optional”不等于“任何组合都合法”。FILE 值必须最终解析到一段确定的字节,否则它就是非法引用。官方 FILE 规范 对字段组合和 Reader 行为做了明确约束。
FILE 三种存储方式
FILE 的核心设计,是把非结构化对象的三种存储方式统一成同一个逻辑类型:
FILE
│
┌────────────┼────────────┐
▼ ▼ ▼
inline self-reference external
字节位于叶子列 字节位于当前文件 字节位于外部 URI
1. Inline:内容直接进入 Parquet 列
inline = <PNG bytes>
content_type = "image/png"
inline 是一个普通 BYTE_ARRAY 叶子列,因此会和其他列一样进入 Parquet Page,并使用该 column chunk 配置的编码和压缩方式。
它适合小图片、短文本、小型附件等对象。优点是数据完全自包含,Reader 不需要额外访问对象存储;代价是大对象可能干扰 Row Group 大小、解码内存和投影扫描。
2. External:指向外部对象
uri = "s3://datasets/images/a.png"
content_type = "image/png"
checksum = "SHA-256:0123..."
checksum 的格式为 <algorithm>:<digest>,规范支持 ETAG、MD5、CRC32、CRC32C 和 SHA-256 五种算法。其中 ETAG 对对象存储场景特别友好——可以直接复用 S3 返回的 ETag 作为完整性标识,不需要重新计算哈希。
也可以配合 offset 和 size,表示对象中的一个切片:
uri = "s3://datasets/packed/blobs-001"
offset = 1048576
size = 65536
对应的字节范围是:
[1048576, 1048576 + 65536)
这种表示适合把许多小对象打包进少量大对象,以减少对象存储 LIST、HEAD、GET 请求和小文件数量。
3. Self-reference:引用当前 Parquet 文件的一段区域
如果 uri 缺失,而 offset 和 size 存在,则引用目标是当前 Parquet 文件自身:
uri = null
offset = 8388608
size = 131072
其含义是:
current_parquet_file[8388608 : 8388608 + 131072]
这些字节不必进入正常的 Parquet 列页;Writer 可以把 blob 放在文件中的其他位置,再通过绝对字节偏移引用它。
Self-reference 的重要性质是可移动性。因为引用中没有写入当前文件的绝对路径,所以整个 Parquet 文件被重命名或搬迁后,内部引用仍然成立。个人觉得这个不错,这样相当于大对象和 Parquet 文件是一个原子单元,有效避免对象存储里面的文件被删了,Parquet 里面还引用着(dangling reference)。
需要注意的是,规范明确规定含 self-reference 的文件不能使用 Parquet modular encryption——这些字节游离在正常的列加密体系之外,无法被列加密覆盖。加密场景下需要避开这种存储方式。
Reader 的解析规则
最终规范可以近似写成下面的读取流程:
if inline is set:
return inline
if uri is set:
if offset is set:
require size
return read(uri, offset, size)
if size is set:
return read(uri, 0, size)
return read_whole_object(uri)
if offset is set:
require size
return read(current_parquet_file, offset, size)
invalid FILE value
完整组合如下:
inline | uri | offset | size | 解析结果 |
|---|---|---|---|---|
| 有 | – | – | – | 使用 inline |
| – | 有 | – | – | 整个外部对象 |
| – | 有 | – | 有 | 外部对象 [0, size) |
| – | 有 | 有 | 有 | 外部对象 [offset, offset+size) |
| – | – | 有 | 有 | 当前 Parquet 文件中的字节范围 |
| – | 有 | 有 | – | 非法 |
| – | – | – | 有 | 非法 |
| – | – | – | – | 非法,如果想表达这一行为 null,应该是在外面的 struct 这一层,设置为 null(def_level=0) |
这里有两个容易误解的地方。
1、只要 inline 存在,Reader 就必须优先使用它。即使同一值还带有 uri、offset 或 size,这些定位字段也只能作为来源信息。Writer 可以选择不同时写入两者(规范允许将其视为互斥),但 Reader 不能忽略 inline 去读取外部对象。
2、只要设置 offset,就必须同时设置 size。设计稿一度允许“从 offset 读到 EOF”,但最终规范明确将 uri + offset 且无 size 定义为非法。这避免不同 Reader 对边界作出不同推断,也使所有范围引用都有确定长度。
FILE 给 AI 数据湖带来了什么
1. 可查询的多模态文件清单
一张训练数据表可以同时包含业务属性与标准 FILE 列:
id | tenant | label | asset(FILE)
查询引擎可以只读取 asset.content_type、asset.size、asset.uri 等叶子列,完成:
- 按图片、音频或 PDF 过滤;
- 统计训练集不同模态的数量与大小;
- 找出 checksum 缺失或引用异常的数据;
- 构建训练 manifest;
- 在真正需要时才获取大对象内容。
这保留了 Parquet 列式投影的优势:检查和筛选文件元数据时,不必读取 inline 或访问外部对象。
2. 小对象打包
对象存储中的海量小文件会带来请求开销、元数据压力和较差的吞吐。FILE 可以让多个逻辑对象共享一个大容器:
uri = "s3://bucket/packs/part-0001.blob"
offset = 8192
size = 32768
每行仍代表独立逻辑文件,并拥有自己的 content_type 和 checksum。查询先在 Parquet 中筛选,再对容器执行有针对性的范围读取。
self-reference 更进一步:容器字节和索引可以处于同一个 Parquet 文件中,形成可整体搬迁的单文件数据包。
3. 稳定的训练输入清单
FILE 允许训练系统把 URI、范围、内容类型和完整性标识与数据集快照一起记录。结合 Iceberg、Delta 或其他表格式的 snapshot,训练任务能够准确回答“某个模型使用了哪些文件引用”。
4. 跨引擎互操作基础
FILE 统一了标准,避免每个引擎都去发明自己的 STRUCT<path, mime, etag>。
说到这里,我感觉 Paimon 惨了。Paimon 的 Blob 是在表格式层自己造的私有方案——把大对象写进单独的 blob 文件,由 Paimon 自己的元数据来管理引用——完全不如 Parquet 直接在文件格式层定义标准来得优雅,目前感觉是白实现了,还和 Parquet 标准打架。
结语
FILE Logical Type 表面上只增加了一个空的 Thrift annotation 和六个约定字段,背后解决的却是 AI 数据基础设施中的一个根本问题:
当数据不再只是数字和字符串,而是散落在列页、Parquet 文件内部和对象存储中的图片、音频、视频与文档时,开放格式应该如何一致地表达“这个值是一份文件”?
Parquet 的答案不是设计一种更复杂的 blob 编码,而是建立一层最小但完整的语义契约:
inline、self-reference和external是同一个类型的不同存储形态;uri / offset / size决定字节位于哪里;content_type决定字节代表什么;checksum提供完整性标识;- Reader 和 Writer 对合法组合拥有共同理解。
这让 Parquet 保持了物理格式上的克制,同时向多模态数据迈出了关键一步。
总的来说,在 AI 时代,Parquet 这位“老人”显然并没有固步自封,FlatBuffers(用其重写 footer 元数据以加速解析的提案)、ALP、Fixed Size List 以及 FILE,无不彰显着 Parquet 想在 AI 时代,继续占领开放文件格式的山头。如果后面 Iceberg 把索引框架标准制定完,那么 Iceberg + Parquet 组合很有可能会成为 AI 数据湖的标准答案,这定会蚕食 Lance 和 Paimon 的一部分市场。
当然,格式垄断肯定是利好数据库从业者了,开发省心省力,做优化也能更有针对性。
好!
湖上多模专家
不敢当不敢当!