从一到无穷大 #106:Nimble 文件格式——超宽表的元数据、编码与索引设计

一张训练表可以只有 sample_id 和 features 几个顶层字段,features 这个 map 里却有上万个 key。从 SQL schema 看,它不宽;从特征投影、编码、元数据和磁盘读取看,它已经是一张超宽表。Nimble 最初面对的就是这类数据。

Nimble 原名 Alpha,是 Meta 开发的列式文件格式,早期实现位于独立 Nimble 仓库,目前开发已迁入 Velox 的 velox/dwio/nimble,通过 reader、writer 和向量化接口与执行层衔接。1 Velox 负责执行,Nimble 负责文件内的数据组织与编码;表快照、跨文件事务、分区维护还要由上层系统处理。2

1 背景、设计动机与业务使用

1.1 训练特征的投影粒度

先用一组构造数据说明问题:

text 复制代码
sample_id    features
1            {101: 0.7, 205: 3.0, 907: 12.0}
2            {101: 0.4,           907: 16.0}
3            {          205: null,          990: 8.0}

多个模型共享这份数据,一个模型只需要 101,另一个需要 205 和 907。用 map 表达特征,增加特征时不必反复修改顶层表结构,但常规 map 的物理表示会把不同 key 的值串在同一条 values stream 中。投影一个 key,可能仍要获取、解密和解码包含其他特征的大段数据。

Meta 在 2022 年的 DSI(Data Storage and Ingestion)介绍中,公开了当时的链路:Tectonic 上的 DWRF Hive tables,经 DPP(Data PreProcessing)服务转换成训练所需的 tensors。DWRF 是 Meta 的 ORC 分支。文中一些重要 ranking 模型只需要已存储字节的 20%---37%,因此特征读取粒度会直接影响存储带宽、网络和预处理 CPU。3

对应的优化是 feature flattening:把每个 feature 拆成可以独立读取的流,模型需要哪个 key,就投影相应数据。这个思路在 DWRF 阶段已经存在,不能把所有 FlatMap 收益都归给 Nimble。2022 年文章报告的 reader 吞吐提升 2---2.3 倍,以及 feature reordering 减少 45%---55% 传输,都是当时那套优化的结果。3

拆开以后,数据读取变细了,元数据却变多了。几万个 feature,每个 feature 还可能带有存在性、null、数组长度等信息;再乘上文件内的 stripe 数量,offset、size、统计和编码描述本身就会成为需要认真处理的数据。

1.2 超宽表对元数据与编码的要求

CIDR 2023 的 Shared Foundations: Modernizing Meta's Data Lakehouse 已经描述了 Alpha。论文明确说,DWRF 对大型 map 做过优化,但数万特征依然带来元数据开销;既有编码和 stripe 结构的部分限制,也很难在保持向后兼容的条件下继续修改。4

Nimble 的几个设计选择都能落回这个问题。

  1. 降低大量 streams 的描述成本。 元数据采用 FlatBuffers,并将文件级目录、stripe group 级目录、可选 sections 分开。FlatBuffers 可以直接在序列化后的 buffer 上访问字段,减少必须先构造一整棵对象树的工作;这不意味着文件打开过程完全没有解压、分配或缓存成本。

  2. 让编码独立演进。 同样一段整数,可能适合 bit packing、delta、dictionary 或 RLE;dictionary 的 alphabet 和 indices 又有不同的数据分布。Nimble 把编码设计成可递归组合的树,并开放选择策略。文件布局不用因为每次增加一种编码就整体重做。

  3. 控制解码工作的粒度。 每条 stream 可以分成多个 chunk,各 chunk 分别编码;读取时,为当前 chunk 建立解码器,按需恢复其中的值。实际内存仍取决于 stripe 的预算、chunk 的大小和编码自己的临时空间。

  4. 并行处理独立特征。 大量独立特征可以并行编码或解码,但嵌套关系依然有依赖。例如要先得到数组长度,才能知道元素流对应的范围。并行度的上限还受内存带宽、I/O 和任务调度影响。2

这些选择同时服务传统分析表。Meta 2024 年的公开介绍把 Nimble 放进可组合的数据管理体系,强调宽表、递归编码和高效解码;它的替代目标包含 Parquet、ORC。目标覆盖面很大,实际收益仍要放到 schema 和访问模式里判断。5

Nimble 早期 README 希望使用者共享一套原生库,通过 bindings 扩展到其他语言,并明确不鼓励各自重新实现规格。统一库可以减少不同 reader 对新编码支持不一致的问题,同时也把采用成本更多地放到原生库集成上。2

1.3 已披露的生产使用与后续扩展

1.3.1 ML 训练:按部分特征读取

多个模型共享一张宽特征表,各自只消费部分 feature keys。Nimble 的 FlatMap 与列投影让 reader 读取需要的特征流,减少无用特征带来的 I/O 和解码;分块编码、解码再降低批量读取成本。这是早期 Alpha 已披露的生产用途。34

1.3.2 Spark 特征维护:内存 FlatMapVector

FlatMapVector 主要适合宽 map 中只取少量 key,以及按特征批量增删 ,对应 Spark 的 feature injection/reaping 场景。6 MapVector 用每行 offset/size 指向展平的 keys/values;FlatMapVector 保存去重 key 集,每个 key 对应一条跨行值向量,并用可选的 in-map 位图记录哪些行存在该 key。78

同一特征的值已经成列,投影时可以直接复用对应向量;支持 FlatMap 的算子也能围绕值向量与 in-map 按 key 操作,减少逐行查 key、重建 map 条目的工作。Nimble selective reader 开启 preserveFlatMapsInMemory 后,还能省去转成 MapVector 的重排与复制。89 收益集中在这些按 key 处理的路径;按 value 过滤或复制大多数特征时,不保证更快。6

1.3.3 Presto / Prestissimo:Cluster Index 支持选择性关联

查询只需要维表中少量 key 时,读取大量维表数据来构建 hash join 的 build 侧会做无用功。Nimble 的 cluster index先将 probe keys 定位为行范围,列式 reader 再取所需字段,支持 index lookup join。2026-04-27 公告称已集成到 Presto / Prestissimo;收益取决于命中比例与读取代价。10

1.3.4 Normalized training data:按引用查特征,再组装训练 batch

沿用 1.1 节的三行表,再构造一条样本 4:假设它与样本 1 使用同一用户、同一版本的特征快照,内容和 label 可以不同,下面只展示 features 这一共享部分。

text 复制代码
sample_id    features
1            {101: 0.7, 205: 3.0, 907: 12.0}
2            {101: 0.4,           907: 16.0}
3            {          205: null,          990: 8.0}
4            {101: 0.7, 205: 3.0, 907: 12.0}

第 1、4 行的整份 features 重复了。真实训练样本中,这部分可能是很长的用户历史或高维特征,随每条样本保存会反复携带同一份数据。Normalized training data 将共享特征独立保存,样本只保留引用,读取时再补齐。Meta 公开的方向就是把特征移到支持索引访问的存储中。11

拆开后的逻辑关系如下,F1、F2、F3 分别标识确定版本的特征快照:

text 复制代码
样本记录(各自的内容、上下文和 label 仍保留)
sample_id    feature_ref
1            F1
2            F2
3            F3
4            F1

共享特征记录
feature_ref  features
F1           {101: 0.7, 205: 3.0, 907: 12.0}
F2           {101: 0.4,           907: 16.0}
F3           {          205: null,          990: 8.0}

这样样本 1、4 共用 F1。样本 2 虽然也有 101、907,值不同,仍然保留为 F2;历史版本或截止范围不同的特征也要保留各自引用。

假设本次 batch 按 [1, 4, 2] 读取,模型只需要 101、907,上层预处理可以这样编排:

  1. 读样本记录。 得到 [F1, F1, F2],保留各自内容、label 和 batch 位置。
  2. 整理查找请求。 将重复引用合并成 F1、F2,同时指定需要的 feature keys 101、907。
  3. 到特征存储取数据。 通过索引定位 F1、F2,投影这两个 key 的数据。服务端可以直接返回编码流,由客户端解码。
  4. 恢复特征。 解码得到 F1→(0.7, 12.0)、F2→(0.4, 16.0),每对值依次对应 101、907。
  5. 按引用回填样本。 按 batch 位置组装为 [1→(0.7, 12.0), 4→(0.7, 12.0), 2→(0.4, 16.0)],再与各自内容、上下文组合为模型输入,label 单独保留为训练目标。

多出来的查找是第 2---3 步,多出来的组装是第 5 步。Nimble 主要优化第 3 步的定位、投影与传输,以及第 4 步的数据恢复成本:cluster/dense index 找到 F1、F2 对应的行,FlatMap 只取需要的 keys,编码投影与切片减少服务端解码、复制和返回的字节。引用去重、历史一致性和按位置回填仍由上层预处理负责;Velox 的 lookup join 等执行组件可以承接关联工作。1012

接入也分阶段。VeloxCon 2026 的近期方案是用 Nimble Serializer 编码已有 SST 的 values,改善 value 内部表示及服务效率;长期方案是用完整 Nimble 文件的 cluster key、dense index 改善规范化训练数据的写入与访问。13 两者都在优化共享特征的存取,特征共享关系本身由上层数据模型建立。

1.3.5 ZippyDB:宽特征值的列式获取

ZippyDB 是建立在 RocksDB 之上的分布式 KV 服务。14 这里需要的是从很宽的历史/特征记录中,只返回模型选择的列或 FlatMap keys。现有 SST 系统可以让 value 使用 Nimble Serializer 表示 ,再由 serde::Projector 在编码 blob 中投影需要的字段;对于逐 key 拆流的 Full FlatMap,可以选择相应 key 的 streams,减少完整 value 的解码和传输。Hybrid FlatMap 的投影粒度更粗,见 2.2 节。1315

这条 SST 接入路径改变的是 value 内部的列式表示;完整 Nimble 文件另有 cluster index + NimbleIndexProjector 的读取路径。公开快照能核对这些组件,未找到完整 SST→Nimble 文件转换器。2026-04-27 公告中的 ZippyDB 生产集成仍在推进,不能把两条路径统称为已经替换全部 SST。1015

1.3.6 Warehouse 默认格式:复用读写与执行基础

SQL 分析、批处理和训练预处理共享数据时,Nimble 的价值是复用格式、原生读写库与 Velox 执行组件,让扫描、特征投影和索引读取使用共同基础。2025 年 @Scale poster 将 warehouse 默认格式列为推进方向,未披露全量迁移完成。2516

独立仓库的早期 README 未承诺 stability/versioning guarantees;迁入 Velox 后,本文仍以固定提交核查,不据源码存在推定对外兼容承诺。2

2 当前设计:文件布局、嵌套数据与读取路径

2.1 文件布局:数据层次与位置目录

先看一张只有 6 行的表。三个字段都用非 null 的 BIGINT,ts 保存整数时间,行号从 0 开始。下面的分组、chunk 大小和字节地址均为构造示例。

text 复制代码
行号    device_id    ts      value
0       101          1000    10
1       101          1001    11
2       101          1002    12
3       202          1000    20
4       202          1001    21
5       202          1002    22
2.1.1 数据部分:Stripe、Stream 与 Chunk

Stripe 保存连续一批行,批内按列组织。 假设每 3 行一个 stripe,stripe 0 保存第 0---2 行,stripe 1 保存第 3---5 行。stripe 0 内,device_id 的 101、101、101、ts 的 1000、1001、1002、value 的 10、11、12 各放入一条 stream;stripe 1 的对应 streams 保存另外三行。17

这个例子中一个字段对应一条值流;复杂字段还会拆出 null、数组长度、FlatMap 存在性等结构流,由 Schema 描述映射。图中省略顶层 ROW 的结构信息。18

Stream 内可以有多个 chunk 。例如 value 的 chunk 0 保存 10、11,chunk 1 保存 12。每个 chunk 的 5 B header 记录 payload 长度和外层压缩类型,payload 保存编码数据;解压后按 encoding prefix 选择 decoder。各 stream 独立形成 chunks,边界不要求对齐。192021

对这个简单标量例子,stripe、stream、chunk 的职责分别近似 Parquet 的 Row Group、Column Chunk、Data Page,具体编码与字节布局不同。

图 1:依据 Nimble acead744 自绘,并按 Velox 73aeb82d 核对默认布局。沿文件 → stripe → stream → chunk 展开,另列 Footer 引用的 FlatBuffers 目录与可选 key 索引、V1 chunk 位置链路。StripeGroup 0 管理 stripe 0、1 的 stream 位置;图中省略 V2 统计、向量索引、checksum trailer 和复杂结构流。值为解码后的逻辑值,大小与位置均为示意。

2.1.2 两级位置目录:Stripe 与 Stream 的位置

Stripes directory 每个 stripe 一项 ,记录行数、文件起点、数据长度,以及 stream 目录所属的 group。实际 FlatBuffers 对象名是 Stripes。17

Stripe 行数 文件起点 数据长度 所属 group
0 3 0 120 0
1 3 120 144 0

据此,第 4 行位于 stripe 1,局部行号为 1;整条 stripe 从文件第 120 字节开始,共 144 B。要进一步找到 value,还需查 StripeGroup 目录。

一个 stripe group 集中管理若干连续 stripe 的 stream 位置。本例 group 0 的目录展开如下,offset 相对各自 stripe 的起点,长度均以字节计:

Stripe Stream 对应字段 相对 offset 长度
0 device_id 0 32
0 ts 32 40
0 value 72 48
1 device_id 0 32
1 ts 32 48
1 value 80 64

StripeGroup 每个 (stripe, stream) 一项,只存位置,数据仍在各 stripe 内。 stripe 1 的 value 地址就是 120 + 80 = 200,从这里读 64 B 可取得这条 stream。17

默认 StripeGroup 是 FlatBuffers 表 :stream_offsets、stream_sizes 为按 stripe 顺序展平的 uint32 数组,另存 stripe_count。Reader 按 stream ID 查位置,字段顺序不必等于磁盘排列顺序。实验性的 stream-major 目录还能将同一 stream 跨 stripe 的 offset/size 序列编码,并共享相同序列;本例使用默认 raw 布局。1722

2.1.3 StripeGroup 分离元数据的收益

上万条 stream 乘以大量 stripe,位置目录也会很大。Nimble 将 stripe 数据与 StripeGroup 目录分开:数据按 stripe 写,目录积累到阈值后按 group 刷出。Footer 只保留各组入口,详细目录可按组加载、缓存;连续数组和相对 stripe 的 32-bit offset 减少位置记录的对象与表示开销,metadata 还可尝试 Zstd 压缩。17

Footer、Stripes 与默认 StripeGroup 都采用 FlatBuffers。目录解压后可直接访问序列化 buffer 中的字段、数组,减少逐项解析、复制与对象构造;数据值仍由 encoding decoder 恢复。1723

分组减少的是 Footer 的细节、目录对象和单次访问需处理的元数据。固定 schema 下,S 个 stripe、每个 C 条 stream,逻辑上仍有约 S × C 对 offset/size;实际字节数取决于表示、共享与压缩。Group 边界由 metadata flush 策略决定,不固定为图中的两个 stripe。17

Postscript 是末尾固定的 20 B,保存 Footer 长度、压缩类型、文件版本、magic 和 checksum。Footer 紧挨在前面,结合文件长度即可定位并按需解压。17

Footer 是元数据入口 ,保存总行数,以及 Schema、Stripes directory、各 StripeGroup 和其他具名 sections 的位置描述;描述包括绝对 offset、落盘 size、压缩类型与解压后 size。Schema 再将字段类型映射到 stream IDs:读 value 时,先确定 stream ID,再去目标 stripe 的目录查字节范围。统计与索引也从 Footer 引用的独立 sections 接入。1718

2.1.5 一次读取:第 4 行的 value

读取第 4 行的 value = 21:

  1. 读 Postscript → Footer,取得 Schema、Stripes directory 与各组目录的位置。
  2. 经 Schema 确定 value 的 stream ID,并收集必要的结构流。
  3. 查 Stripes directory :第 4 行位于 stripe 1,局部行号为 1,stripe 起点为 120,目录属于 group 0。
  4. 查 StripeGroup 0 :该 stream 相对 offset 为 80、长度为 64,读取文件字节 200 开始的 64 B。
  5. 按 chunk header 与 encoding 解码,跳过第一项 20,取得第二项 21,按 Schema 组装结果。

普通路径先读取选中 stream 的编码字节,再处理所需 chunks;找到某行不等于只读该行的几个字节。更细的 I/O 需要索引与 reader 支持,见 2.4 节。以上是逻辑步骤,预读、缓存和合并读取会改变实际请求次数。1718

逐字节字段、目录下标与地址公式见字段与寻址参考。

2.2 Schema、null 与 FlatMap

Schema 描述逻辑类型与 stream 的映射;其中 SchemaNode.offset 是 stream ID,并非文件字节地址。18

逻辑对象 结构 stream 子数据
Scalar typed values 编码内部表达 null
ROW nulls Bool 各字段 subtree
ARRAY nullable UInt32 lengths elements subtree
MAP nullable UInt32 lengths keys、values subtrees
FlatMap map nulls Bool 每 key 的 in-map 与 value subtree

这是逻辑拆流关系,常量结构流可按策略省略;源码另有 ArrayWithOffsets、SlidingWindowMap 等扩展。Nullable scalar 通常在同一物理 stream 中,通过 NullableEncoding 保存 non-null values 与 non-null Bool 子编码;这与 ROW 自己的 null stream 分属编码和结构两个层次。1824

图 2:2026-05-01 Velox 官方原图。6 上图串联不同 key 的值,下图将每个 key 独立组织;占位值仅说明逻辑关系,不代表 Nimble 原样落盘。

FlatMap 自身有 map nulls stream,每个 key 有 in-map Bool stream 与 value subtree,key 名进入 schema。读取 101 时,只收集它及所需父结构的 streams,跳过其他 key 的值。in-map 区分 key 缺失与 value 为 null:原例第二行缺少 205,第三行有 205 但值为空,两者必须保留。1824

代价是 key 越多,stream 和 schema 越大,动态 key 还带来文件间 schema 差异。Writer 支持预定义 keys 和 maxFlatMapKeys;当前 BatchReader/FieldReader 将 FlatMap 限于顶层字段。18

Serializer 的 Hybrid FlatMap 让多个 key 共用 streams,服务端按组投影、客户端再筛 key,降低拆流数量但增加多读。它用于 serialized-value 路径,普通文件 writer 仍采用逐 key 拆流的 FlatMap。151825

2.3 级联编码、选择策略与 OpenZL

以一个 chunk 中的 [200, 200, null, 200, 404, 404, null, 200] 为例,指定下面这棵编码树:202426

图 3:依据 Velox 73aeb82d 自绘。整棵树序列化在同一 stream 的一个 chunk payload 内,分叉不增加 tablet streams。示例省略 header、长度和可选压缩,仅用于说明机制,不代表默认选择结果或压缩率。

Nullable 先生成 non-null mask = [1, 1, 0, 1, 1, 1, 0, 1],另存 6 个非空值。Dictionary 再将这 6 个值拆成 alphabet = [200, 404] 与 indices = [0, 0, 0, 1, 1, 0]。

级联编码就是对子数据继续分别选编码。 本例 alphabet 用 Trivial,低位宽的 indices 用 FixedBitWidth,Bool mask 用 Trivial 的位打包。父节点负责拆分、重建,子节点处理各自的数据分布,分别保存类型与计数;RLE 的 run lengths/run values、Delta 的 deltas/restatements 也能这样继续编码。202426

解码时先恢复 alphabet 和 indices,查表得到 6 个非空值,再按 mask 放回 8 个位置并补 null。文件布局定位 chunk,编码树解释 payload。

实际组合由 EncodingSelectionPolicy 选择,默认成本为估计字节数乘 read factor,也允许候选自定义估计。Writer 可复用首个 chunk 的 encoding layout,不适用时重新选择;层数增加仍有元数据和解码代价。CIDR 论文的 history loopback database 是 Meta 当时的外部反馈系统,不是文件库自带的服务。42721

编码之后还可继续通用压缩。Nimble 支持编码内部压缩及外层 chunk compression,后者当前支持 Uncompressed、Zstd、LZ4。1828 2026-07-05 开放的 OpenZL 集成用 codec DAG 保留 typed information;官方报告,在 Nimble 已有预处理之上,backend compressor 相比 Zstd 体积约减少 10%,CPU 负载接近。29

这不是 Nimble 文件与任意 Zstd 文件的整体对比。OpenZL 论文还区分了服务 encoded execution 的 Nimble 变换和服务压缩的 OpenZL 变换,Table 6 将该用例标为 Trained: No,不能把编码历史反馈与 OpenZL graph training 混为一谈。30

2.4 Velox 读写、索引与编码后切片

2.4.1 Velox 读写路径
text 复制代码
Velox vectors → FieldWriter → typed stream buffers
  → StreamChunker(启用多 chunk 时)
  → EncodingFactory + selection policy → ChunkedStreamWriter
  → TabletWriter → stripe / metadata / footer / postscript

读路径由 TabletReader 解析目录,StreamLoader 获取投影 streams,chunk decoder 还原值,FieldReader 按 schema 重建 vectors。Selective reader 还接入 ScanSpec、filter 与 lazy loading,各入口的优化能力并不相同。1831

2.4.2 文件内索引:先由 key 找行,再由行找字节

查询 device_id = 202 AND ts = 1001 时,索引可先找到第 4 行,再沿 2.1 节的位置目录读取 value;向量检索则先找相近向量所在的行。Velox 73aeb82d 的索引与剪枝能力如下:1732

索引或剪枝能力 组织方式 查找条件 结果
Cluster Index 分层 partition 边界与 key chunks 有序数据上的 key 或 key 范围 连续行范围
Hierarchical ClusterIndex 按键列组织的分层序列 有序整数复合键 连续行范围
Hash Index 哈希桶 等值 key 匹配 row IDs
Sorted Index 排序的 key + row_id key 或 key 范围 匹配 row IDs
FAISS VectorIndex IVF 或 HNSW 向量 Top-K 近邻 邻居 row IDs 与 score
ChunkStats V2 min/max Chunk 取值范围统计 标量过滤谓词 可排除的 chunks

前四项从 Footer 引用的 columnar.indexes 进入,partition、key chunks 等独立存放、按需加载。Cluster 家族利用已有排序,Hierarchical 按整数复合键逐列下降;Hash 保存 buckets、exact keys 和 row IDs,可附加 Bloom filter;Sorted 独立排序 encoded_key || row_id,原数据无需按此键重排。1732

VectorIndex 使用独立 columnar.vector.index 与 VectorIndexDirectory。输入为顶层固定维度的 ARRAY<REAL>,支持 squared L2、cosine、dot product,拒绝 null 向量与 null 元素。配置覆盖 IVF Flat/SQ8/PQ/RaBitQ、HNSW SQ8;RaBitQ FastScan 在开源 CMake 中禁用。该能力仍标为实验性,读取会加载选中列的整个 FAISS 索引 blob;源码存在不代表 SQL 优化器会自动规划 ANN。323322

ChunkStats V1 保存累计 chunk 行数、字节位置与 null counts,V2 增加 min/max;Selective reader 已用于特定顶层 scalar filter 的 chunk 跳过,块内仍需精确过滤。数组、FlatMap 等结构还要将顶层行范围映射到子流值范围,不能直接套同一行号。1734

这些能力共同组成 查询条件 → 候选行或 chunks → 位置目录 → 投影读取、解码与精确过滤,由 writer 配置保存、reader 按能力使用。实际读取的字节数取决于读取路径。图 1 只画 key 索引与 V1 位置链路;完整实现说明见索引核查。

2.4.3 索引查找与选择

图 4:2026-04-27 官方原图。10 展示 stripe group、index partition 与 key chunks;主线 section 名现为 columnar.indexes,chunk stats 有 V1/V2。图中 root in footer 是简写,实际由 Footer 引用独立 metadata bytes。

Cluster Index 将复合键编码成可按字节比较的字符串。根节点按边界选 partition,再按 chunk 边界找到 key chunk,最终得到 file-level row ranges,交给列 reader。32 官文所说典型点查的"两次读"指 index group metadata 与 key chunk,不含最终投影列数据的 I/O。10

Dense 索引的自动选择还比较粗:无名称约束时,DenseIndexRegistry::findIndex() 返回第一个匹配 reader;内置 reader 要求查询列列表及顺序完全一致,未比较选择率、覆盖关系、缓存与随机 I/O 成本。这是 Hash/Sorted 所属 dense 家族的匹配路径,Cluster 另属一族。3536

当前 Cluster 配置等 writer options 仍标为实验性,不能由源码可用推定对外稳定承诺。32

2.4.4 编码后切片与并行读写

2026-09-08 的 StreamSlicer 方案将顶层 row range 递归映射到 non-null、length、in-map 决定的子流范围,再切片编码数据,减少整 stripe 返回及构造、过滤、重序列化 RowVector 的开销。压缩 chunk 仍须先解压,缺少 native slice 的编码会回退到请求范围的解码与重编码。12

主线已有 serde::StreamSlicer、EncodingSliceFactory 和 NimbleIndexProjector;Hybrid FlatMap 暂不支持部分行范围切片,只能让完整 batch 原样通过。183738

CPU 并行读写需要 executor 与预算:写端并行编码 stream buffers,读端支持 ROW 子字段、FlatMap-as-struct values 并行解码;默认配置仍为串行。2026-10-05 官文说明了共享预算与父子依赖,也明确 wall time 减少不等于 total CPU 减少。21182239

GPU-friendly 目前仍是设计方向。早期 README 将暴露 decoding-tree metadata 以规划 GPU kernels 标为尚未实现,本轮主线也未找到完整 Nimble GPU 文件解码链路。2

3 不同文件格式面向的负载

我在《从一到无穷大 #55 提升 Lakehouse 查询性能:数据层面》中整理过编码与压缩、数据排布、索引、预计算和缓存等优化方法。40 数据排布与业务访问极其相关:按设备聚集适合查单设备历史,按时间聚集适合跨设备查短窗口。文件格式与配套 Reader/Writer 则负责把这些数据存得多省、在目标负载下读得多快。这里关注的共同需求是宽列、少列读取,时序指标与 ML 特征都存在这个问题,但后续的行访问方式并不相同。

格式 主要目标负载 重点优化的访问
Parquet 跨引擎的嵌套数据分析 少列的批量扫描
ORC Hive 等数仓分析 带过滤的大规模扫描
TsFile 设备/实体的时序数据 指定序列的时间范围查询
Lance ML 与多模态数据 检索或采样后的随机取行
F3 数据与压缩需求持续变化的分析系统 可独立调节读取与解码粒度的访问
Nimble 超宽、稀疏的训练特征 部分特征扫描与按业务 key 取行

3.1 Parquet 与 ORC:面向批量分析的列式存储

Parquet 最初强调的是复杂嵌套数据的列式表示、高效压缩,以及不同 Hadoop 计算框架共享一种格式。它沿用 Dremel 的嵌套表示思路,以 Row Group、Column Chunk、Page 组织数据,适合只读部分列的大批量分析。4142 ORC 则从加速 Hive 出发,优化大规模流式读取,并通过内置索引跳过无关数据;Stripe 内按类型拆分 streams,配合编码、统计和 Row Index。4344

超宽表放大了这些设计的元数据成本。Parquet 的 Footer 集中描述各 Row Group 的叶列:一万个叶列乘一百个 Row Groups,就是约一百万个 Column Chunk 描述,查询只要十列也不会让其余描述消失。当前 Velox Reader 先整体反序列化这份 Thrift metadata,这会增加文件打开时的 CPU 与内存开销。45 ORC 已把 stream 细节分散到各 Stripe Footer,但访问宽 stripe 时仍需处理相应的目录和索引;两者的问题不能完全混为一谈。44

Parquet 的 Footer 能直接定位 Column Chunk,可选 OffsetIndex 还能定位 page。当前 Velox 常规读取路径排入的是目标 Column Chunk 的完整范围,粗粒度读取包含 Reader 实现的选择,并非格式只能找到 Row Group。4647 此外,普通 map 中不同 key 的值共用存储;要按特征细读,还需展开成列或采用 DWRF FlatMap 这样的扩展,再处理随之增加的元数据。3

3.2 TsFile:面向设备与序列的时序存储

TsFile 论文从 IoT 数据的特点出发:不同序列需要不同编码,稳定的设备标识适合独立索引,端、边、云之间合并文件又应尽量避免重复解码与重编码。按序列压缩、分层设备标识、ETL-free compaction 是它的主要动机,兼顾时序读写效率与跨层共享文件。48

设备与序列索引先缩小目标,Chunk/Page 统计再裁剪时间范围;时间和值可以分别采用适合自身分布的编码。Aligned 布局让多个指标共享时间轴,non-aligned 序列保留独立时间轴。它与 Nimble 都能服务宽数据的少列读取,但 TsFile 直接利用时序模型,Nimble 更关注动态 feature keys 与复杂嵌套值。Page 剪枝也不等于按 page 发起 I/O:本文核对的 TsFile 2.4.0 常用 Java 路径仍先加载整个目标 chunk。4950

3.3 Lance:兼顾扫描与随机取行

Lance 的官方设计强调既能高效扫描,也能高效随机访问。训练采样、向量检索或其他索引返回 row IDs 后,还要取回相应的向量、文本等字段,顺序扫描友好的大块压缩会在这里产生读放大。Lance v2 取消统一 Row Group,各列独立分页,列元数据也可以独立读取。51

Lance 2.1 进一步将结构组织与压缩编码分开:Mini-block 用小块控制随机读取的解码范围,Full-zip 面向较大值,将结构和值交错组织。5253 目标是让按行定位与压缩共存;字典、位置索引的加载仍有成本,不能把文件层的随机读取能力直接等同于上层向量索引能力。

3.4 F3:编码演进与跨系统兼容

F3 论文首先面对的是一个演进问题:新的压缩方法不断出现,固定规格却要求所有读取端同步支持新编码。它将解码器随文件以 Wasm 形式携带;读取端有匹配的 native decoder 就直接使用,否则通过 Wasm 执行,从而降低新编码进入不同系统的门槛。54

效率上,F3 将 I/O 单元、编码单元和字典作用域分别配置,避免一个分组大小同时约束读取、解码和压缩;各列元数据采用独立 FlatBuffer,支持按需访问。它更像是在提供可持续演进的格式框架,而非专门优化某一种查询。Wasm 路径仍有运行成本,官方实现也仍是研究原型。55

3.5 Nimble:超宽特征的存储与投影

CIDR 2023 对 Alpha,也就是后来的 Nimble,给出的起点很具体:训练表有数万特征,DWRF 的元数据与既有格式约束妨碍继续优化。4 因而 Nimble 将位置目录分组保存,用 FlatBuffers 降低访问元数据的解析与对象构造成本,FlatMap 按 key 分流,递归编码分别处理值、字典、编号等子数据。

这种分层将数据分块与详细元数据加载的粒度分开 :较多的小 stripe 可以由一个 StripeGroup 集中管理 stream 位置。加载 Stripes directory 后,就能取得全部 stripe 的起点、长度、行数和所属 group,详细位置目录再按 group 加载。这样可以增加 stripe 数量,而不必把每个 stripe 的所有 stream 描述都放进文件级 Footer。默认布局仍为每个 (stripe, stream) 保存位置项;group 越大,单次加载的详细目录也越大。17

当前代码不固定每个 StripeGroup 的 stripe 数量 :每写完一个 stripe,就检查估算的元数据大小,默认达到 8 MiB 后结束该组,阈值可通过 metadataFlushThreshold 调整。按现有估算规则,若每个 stripe 都有 10,000 条 stream,约 65 个 stripe 就会触发一次分组;这里判断的是估算大小,非压缩后的实际字节数。关闭文件时,未达到阈值的剩余 stripe 也会写成一组。1722

Parquet 同样可以筛选 RowGroups,只读取选中组的数据。当前 Velox 的顺序是先整体反序列化 Footer 中的 RowGroup 与 ColumnChunk 元数据,再根据 split 和统计信息选组;被排除组的部分元数据对象随后才释放。Nimble 的分层目录让粗粒度 stripe 定位可以先于 stream 目录的加载,避免这部分工作全部集中在文件打开阶段。1745

这些设计服务少量特征扫描,后续索引又补充了按 key 找行的路径;FlatMapVector 与编码后投影、切片进一步减少文件到执行器之间的转换。812 它可以承载时序数据,但不会自动获得 TsFile 的时间轴模型;普通 Reader 仍可能加载完整的目标 stream,内置索引也不能代替业务数据的物理聚集。17

4 结束语

存储文件格式加上配套 Reader/Writer,最终要对压缩比、扫描吞吐、点读延迟和 I/O 次数负责。元数据、索引也要计入存储量;查询既要看读了多少字节,也要看发了多少请求、其中几轮必须串行等待。压缩更小但读得更多,或者少读字节却增加大量零散请求,都要放回具体负载里判断。

Nimble 已有足够吸引我的地方。Meta 报告的 Nimble + OpenZL,在同一 Nimble 处理链中,相比使用 Zstd 的后端压缩体积约小 10%,CPU 负载接近;这并不是 Nimble 与 Parquet 的整文件对比。29 StripeGroup 分组保存位置目录,FlatBuffers 减少元数据解析与对象构造。对象存储上的少列读取,所需元数据能由一次尾读取得并直接复用、投影数据又能合成一个读取范围时,可以做到两个 GET;目录命中缓存还能省去相应读取,分散的列则可能增加请求。17

对我来说,这已经是一个好的格式,但还不足以让我直接替换现有存储。从执行层接入看,替换也没有那么困难:Velox 的 TableScan 经 connector 和格式 Reader 将数据交付为 RowVector,上层算子可以继续使用相同的向量接口。真正迁移时仍需处理存量文件、写入端和类型支持。5657

Parquet 集中式 Footer 对今天的超宽表确实显得笨重。不过,我了解 Nimble 的重点,是 Velox 对它的适配能否满足我的时序查询,以及这些索引接口能否用于给 Parquet 扩展新的索引结构。目前这些内置索引还不足以直接承接我的时序方案,相关扩展接口却值得用:Velox 有格式无关的 IndexReader,也允许 Hive connector 按文件格式注册索引读取实现。5859

可行的方向是先通过业务索引得到候选 row IDs,绑定文件及其版本,换算为 Row Group 内的行号,再利用各投影列的 Parquet OffsetIndex,找到覆盖这些行的 page 偏移和长度,合并范围后读取;文件必须保存 OffsetIndex,Reader 还要处理字典页与嵌套列的恢复,最后解码并精确过滤。46 这条路径主要解决读放大。当前 Velox 的 Parquet Reader 还没有打通这条外部 row IDs 驱动的按页读取链路,需要补上索引接入和读取调度,不能只换一个接口就算完成。4758

而且,命中的行若散落在大量 pages 和文件里,少读字节之后,I/O 次数仍可能很多。最终还要让经常一起查询的数据尽量聚在一起,借助 Liquid Clustering 这类按聚簇键维护布局的机制。60 读取端再做范围合并、并发拉取和按访问模式预取,并尽量复用元数据、索引和数据缓存。原有排序实验中,仅改变物理排列,就让同一查询的候选 Row Groups 从 32 个降到 1 个;它只验证了剪枝范围,但已经说明数据排布为什么离不开业务访问。对我的场景,这些仍是性能提升的关键,换文件格式只能解决其中一部分。

5 参考资料

1 Nimble 迁入 Velox 的官方仓库说明,固定提交

2 The Nimble File Format,固定提交 README

3 Scaling data ingestion for machine learning training at Meta,2022-09-19

4 Shared Foundations: Modernizing Meta's Data Lakehouse,CIDR 2023

5 Composable data management at Meta,2024-05-22

6 FlatMapVector Adoption for Scaling High-Performance AI/ML Data Pre-Processing,2026-05-01

7 Velox MapVector:每行 offsets/sizes 与展平 keys/values

8 Velox FlatMapVector:distinct keys、每 key 值向量与 in-map

9 Velox Nimble FlatMapColumnReader:preserveFlatMapsInMemory

10 Nimble Cluster Index: Efficient Indexed Lookups on Columnar Data,2026-04-27

11 How Meta Scaled AI Training Storage via Data Normalization,@Scale 官方演讲摘要,2026-06-22

12 Slicing Encoded Streams Efficiently in Velox Nimble,2026-09-08

13 VeloxCon 2026 Agenda,Velox IO -- Nimble Index & Open Table Format Support

14 How we built a general purpose key value store for Facebook with ZippyDB,2021-08-06

15 Velox Nimble Serializer / Projector:序列化 values 与编码内列投影

16 @Scale: AI & DATA 2025,Nimble: A Flexible File Format for Training Data Efficiency poster

17 Velox Nimble tablet:Postscript、Footer、StripeGroup、TabletReader/Writer、ChunkStats

18 Velox Nimble:Schema、FieldWriter、FieldReader 与读取路径

19 Velox Nimble ChunkHeader.h:5 B chunk framing

20 Velox Nimble EncodingPrefix.h:默认与 compact row-count prefix

21 Velox Nimble writer:Writer、StreamChunker、ChunkedStreamWriter

22 Velox WriterOptions:实验目录、向量索引与并行预算

23 FlatBuffers 官方说明:直接访问序列化数据

24 Velox Nimble NullableEncoding.h

25 Hybrid FlatMap: Group-Level Feature Projection in Nimble,2026-10-09

26 Velox Nimble DictionaryEncoding.h

27 Velox Nimble 编码选择策略

28 Velox Nimble compression 源码与说明

29 Making OpenZL Available in Nimble OSS,2026-07-05

30 OpenZL: A Graph-Based Model for Compression,§7 与 Table 6

31 Velox Nimble Selective Reader

32 Velox Nimble index:Cluster、Hierarchical、Hash、Sorted、Vector 与索引匹配

33 Velox Nimble CMake:开源构建禁用 RaBitQ FastScan

34 Velox SelectiveNimbleReader:基于 chunk bounds 的谓词剪枝

35 Velox Nimble DenseIndexRegistry.cpp:首个匹配索引

36 Velox Nimble DenseIndexFactory.cpp:列列表与顺序匹配

37 Velox Nimble EncodingSliceFactory.cpp:原生切片与物化回退

38 Velox Nimble StreamSlicer:结构化 row-range 切片及 Hybrid 限制

39 Parallel Encoding and Decoding in Nimble: Faster Flushes and Reads,2026-10-05

40 从一到无穷大 #55 提升 Lakehouse 查询性能:数据层面,2025-11-07

41 Apache Parquet Motivation:嵌套数据、压缩与跨框架访问

42 Apache Parquet:File Format

43 Apache ORC Background:Hive 加速与大规模流式读取

44 Apache ORC v1 Specification

45 Velox ParquetReader:尾读、Thrift 元数据构造与清理

46 Apache Parquet:Page Index

47 Velox ParquetData:目标 Column Chunk 范围排队读取

48 Apache TsFile: An IoT-native Time Series File Format,PVLDB 2024

49 TsFile 2.4.0 TsFileSequenceReader:选择性元数据与完整 chunk 加载

50 TsFile 2.4.0 ChunkReader:加载 chunk 后的 page 剪枝与解压

51 Lance File Format

52 Lance File 2.1: Smaller and Simpler,2025-03-27

53 Lance File Format:Encodings

54 F3: The Open-Source Data File Format for the Future,作者公开 PDF

55 F3 官方仓库与研究原型声明

56 Velox TableScan:从 DataSource 取得 RowVector 批次

57 Velox FileSplitReader:按 FileFormat 选择 ReaderFactory

58 Velox Reader.h:通用 IndexReader 与默认 createIndexReader

59 Velox Hive IndexReader:按文件格式注册索引读取实现

60 Databricks:Use liquid clustering for tables

相关推荐
boy快快长大1 小时前
MySQL 转 PostgreSQL 使用指南
数据库·mysql·postgresql
晴天¥1 小时前
Oracle集群中是该如何管理控制文件和日志文件组?
数据库·oracle
NineData1 小时前
NineData 社区版 V5.4.0 上线:715 条复制对比链路,AI Agent 新增表结构设计发布能力
大数据·数据库·人工智能·mysql·数据库管理工具·ninedata·数据库迁移工具
云策数链1 小时前
用友 T+Cloud / U8 项目交接时的账套健康度核查清单
数据库·oracle·erp·用友·云策数链
Tt有了t1 小时前
Redis的数据结构
数据结构·数据库·redis
ss2731 小时前
PostgreSQL咨询锁的使用
数据库·postgresql
ycydynq1 小时前
django 添加信息时,对外键设置条件
数据库·python·django
m0_639906942 小时前
MySQL 索引底层原理|B + 树、聚簇索引、非聚簇索引、覆盖索引
数据库·mysql
全栈陈序员2 小时前
【Python】基础语法入门(二十八)—— 数据库操作入门:用 SQLite 持久化数据
数据库·人工智能·python