一张训练表可以只有 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 的几个设计选择都能落回这个问题。
-
降低大量 streams 的描述成本。 元数据采用 FlatBuffers,并将文件级目录、stripe group 级目录、可选 sections 分开。FlatBuffers 可以直接在序列化后的 buffer 上访问字段,减少必须先构造一整棵对象树的工作;这不意味着文件打开过程完全没有解压、分配或缓存成本。
-
让编码独立演进。 同样一段整数,可能适合 bit packing、delta、dictionary 或 RLE;dictionary 的 alphabet 和 indices 又有不同的数据分布。Nimble 把编码设计成可递归组合的树,并开放选择策略。文件布局不用因为每次增加一种编码就整体重做。
-
控制解码工作的粒度。 每条 stream 可以分成多个 chunk,各 chunk 分别编码;读取时,为当前 chunk 建立解码器,按需恢复其中的值。实际内存仍取决于 stripe 的预算、chunk 的大小和编码自己的临时空间。
-
并行处理独立特征。 大量独立特征可以并行编码或解码,但嵌套关系依然有依赖。例如要先得到数组长度,才能知道元素流对应的范围。并行度的上限还受内存带宽、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,上层预处理可以这样编排:
- 读样本记录。 得到
[F1, F1, F2],保留各自内容、label 和 batch 位置。 - 整理查找请求。 将重复引用合并成
F1、F2,同时指定需要的 feature keys101、907。 - 到特征存储取数据。 通过索引定位 F1、F2,投影这两个 key 的数据。服务端可以直接返回编码流,由客户端解码。
- 恢复特征。 解码得到
F1→(0.7, 12.0)、F2→(0.4, 16.0),每对值依次对应101、907。 - 按引用回填样本。 按 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
2.1.4 文件尾部:Postscript 与 Footer
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:
- 读 Postscript → Footer,取得 Schema、Stripes directory 与各组目录的位置。
- 经 Schema 确定
value的 stream ID,并收集必要的结构流。 - 查 Stripes directory :第 4 行位于 stripe 1,局部行号为 1,stripe 起点为
120,目录属于 group 0。 - 查 StripeGroup 0 :该 stream 相对 offset 为
80、长度为64,读取文件字节200开始的64 B。 - 按 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:实验目录、向量索引与并行预算
24 Velox Nimble NullableEncoding.h
25 Hybrid FlatMap: Group-Level Feature Projection in Nimble,2026-10-09
26 Velox Nimble DictionaryEncoding.h
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:嵌套数据、压缩与跨框架访问
43 Apache ORC Background:Hive 加速与大规模流式读取
44 Apache ORC v1 Specification
45 Velox ParquetReader:尾读、Thrift 元数据构造与清理
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 剪枝与解压
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
56 Velox TableScan:从 DataSource 取得 RowVector 批次
57 Velox FileSplitReader:按 FileFormat 选择 ReaderFactory
58 Velox Reader.h:通用 IndexReader 与默认 createIndexReader