三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

An Empirical Evaluation of Columnar Storage Formats

An Empirical Evaluation of Columnar Storage Formats

An Empirical Evaluation of Columnar Storage Formats

《An Empirical Evaluation of Columnar Storage Formats》评估的是 Parquet 和 ORC 这两类主流开源列式文件格式。论文没有把结论写成“谁更快”,而是把文件格式拆成多个内部维度:layout、encoding、compression、metadata、indexing、nested representation、ML workloads 和 GPU decoding。

这篇整理基本沿用论文结构。技术细节上会额外展开几个容易被误解的点:PAX-style layout、宽表投影的 metadata 瓶颈、对象存储下的 index/metadata 布局、Parquet definition/repetition level 与 ORC presence/length 的差异,以及这些设计在 ML 负载下为什么没有绝对优劣。

1 Introduction

Parquet 和 ORC 成熟于 Hadoop 生态早期。它们解决的核心问题是:让不同查询引擎在大规模分析场景下共享列式数据文件,并通过列裁剪、压缩、向量化扫描和统计信息剪枝提升分析查询性能。

论文重新审视这两个格式,是因为外部条件已经变化:

存储介质:本地 SSD 和云对象存储的带宽显著提升。访问介质:S3 这类对象存储高带宽但高延迟,小范围随机读成本高。工作负载:除 SQL scan/filter 外,出现了宽特征表、embedding、vector search、图片/音频/视频等 ML 数据访问模式。硬件路径:CPU 解码、SIMD、GPU decoding 和 PCIe 传输成本开始影响格式设计。

因此,传统的“压缩率更高就更好”或“端到端查询快就更好”都不够精确。一个列式格式的表现,需要拆到 encoding、metadata、index layout、nested model 和 hardware path 层面分析。

列式存储的基本优势来自三个方面:

列裁剪:查询只读需要的列。列压缩:同一列数据类型和分布相近,更容易编码和压缩。向量化处理:连续列值适合 batch decoding 和 vectorized execution。

Parquet 和 ORC 都可以理解为 PAX-style layout 的工程实现。PAX 是 Partition Attributes Across,核心思想是先按行分区,再在每个分区内部按列连续存放。

row group / stripe
├── column A values
├── column B values
├── column C values
└── ...

Parquet 的典型结构是:

file
├── row group 1
│   ├── column chunk A
│   │   ├── page 1
│   │   └── page 2
│   └── column chunk B
└── footer

ORC 的典型结构可以简化为:

file
├── stripe 1
│   ├── streams
│   └── stripe footer / index
├── stripe 2
└── file footer

这类布局适合传统 OLAP:少量列投影、大量行扫描、列级压缩和统计信息剪枝。但当访问模式变成超宽投影、低选择率随机回表或大对象读取时,PAX-style layout 里的 row group、stripe、metadata 和 index 的设计会成为新的瓶颈。

3 Feature Taxonomy

论文先建立了一个 feature taxonomy,用来比较 Parquet 和 ORC 的格式内部差异。这个部分比直接跑 benchmark 更重要,因为它能解释性能差异来自哪里。

3.1 Layout

Parquet 和 ORC 都按行分区组织列数据,但分区、page/stream、footer/index 的组织方式不同。Parquet 更强调 row group、column chunk、page 和集中 footer;ORC 使用 stripe 和 streams,部分索引或统计信息靠近 stripe。

这个差异影响两个路径:

本地扫描:细粒度统计信息可以减少无效数据读取。对象存储:元数据和索引越分散,越容易增加 range GET 数量。

因此,细粒度 index 不是孤立优势。它必须和物理存放位置一起评价。

3.2 Encoding

Parquet 和 ORC 都支持列式编码,例如 dictionary encoding、RLE、bit-packing 等。论文的关键观察是,真实数据中很多列具有低 NDV,也就是 distinct value 数量相对行数较低。这类数据天然适合字典编码。

字典编码之后,原始值会变成 dictionary code。此时格式效率取决于两层:

dictionary 本身是否有效;
dictionary code 的整数编码是否容易快速解码。

Parquet 更积极地默认使用字典编码,因此在很多真实数据场景下能获得较好的文件大小和解码速度。ORC 的编码策略更复杂,在某些场景能压得更小,但也可能带来更高解码成本。

3.3 Block Compression

Parquet 和 ORC 都支持通用块压缩。论文的判断是,现代硬件下块压缩不应该被简单视为默认收益。

如果压缩节省的 I/O 时间小于解压 CPU 成本,扫描会变慢。压缩策略应该同时看:

列类型;
列基数和分布;
是否 CPU-bound;
存储介质是本地 SSD 还是对象存储;
查询是否经常投影该列;
解码路径是否能向量化或 GPU 化。

对象存储和 GPU 场景可能改变这个判断。对象存储上减少传输量可能更有价值;GPU 场景下,减少 PCIe 传输有时也会让更高压缩率变得合理。

3.4 Metadata, Indexing and Filtering

Parquet 和 ORC 都有 min/max statistics、zone map、Bloom Filter、page index 等辅助结构。它们的目标是减少无效扫描,但实际效果受数据分布、索引粒度和物理布局影响。

常见边界是:

zone map:对聚簇性较好的列更有效;值分布很散时,min/max 剪枝能力有限。Bloom Filter:更适合点查,不是范围索引的替代品。page index:可以提高局部剪枝能力,但必须考虑索引读取位置和对象存储请求数。

ORC 的细粒度 zone map 在本地 SSD 上可能有效,但如果相关 metadata 分散在多个位置,在 S3 上可能产生更多 range GET,抵消剪枝收益。

3.5 Nested Data Model

Parquet 和 ORC 的嵌套表达是一个核心差异。

Parquet 采用 Dremel 模型:只把 leaf fields 作为列存储,每个 leaf column 携带 definition level 和 repetition level。

definition level:表示 root 到 leaf 路径上 optional/repeated 节点定义到了哪一层。repetition level:表示 repeated/list 中当前值是新记录开始,还是继续当前 repeated group。

ORC 更接近结构流和值流分离:

optional field:使用 presence stream 表示是否存在。repeated/list field:使用 length stream 表示每行元素个数。

例如 schema 是:

optional group name {optional string first;optional string last;
}repeated string tags;

三行数据:

row1 = {"name": {"first": "Mike", "last": "Lee"}, "tags": ["a", "b"]}
row2 = {"name": null, "tags": []}
row3 = {"name": {"first": "Joe"}, "tags": ["c"]}

Parquet 会把 leaf column 和 levels 放在一起:

name.first:Mike(D=2), null(D=0), Joe(D=2)name.last:Lee(D=2), null(D=0), null(D=1)tags:a(R=0,D=1), b(R=1,D=1), null(R=0,D=0), c(R=0,D=1)

ORC 更像这样:

name.present:        true, false, truename.first.present:  true, true
name.first.values:   Mike, Joename.last.present:   true, false
name.last.values:    Leetags.length:         2, 0, 1
tags.values:         a, b, c

Parquet 的优势是 leaf column 自包含。只读 name.first 时,主要读取 name.first.values 和它的 levels,不必额外读取 name 的 presence stream。ORC 的优势是共享结构信息有独立表达,父节点 presence 不需要隐含在多个 sibling leaf 的 levels 里。

Parquet definition levels 是按 leaf column 单独编码和压缩的,但每个 leaf 的 definition level 都携带 root 到该 leaf 路径上所有 optional/repeated 节点的定义状态。因此多个 sibling leaf 会重复携带共同父节点的存在性信息。

optional group profile {optional string f1;optional string f2;...optional string f100;
}

如果某一行 profile = null,100 个 leaf column 的 definition levels 都会表达这个事实:

profile.f1.D = 0
profile.f2.D = 0
...
profile.f100.D = 0

这不是复制全局 profile.present bitmap,而是共同父节点状态被隐含在每个 leaf 的 D-level 序列里。物理上是否明显放大,取决于 levels 的可压缩性:连续 null 多时 RLE 可以压得很好;null 分布随机、嵌套深、sibling leaf 多时,重复结构信息更容易转化为文件大小。

这种重复本身不会导致读取异常。正常 reader 通常按当前 projection 的 leaf column 自己的 levels 重建值序列,不会为了只读一个 leaf 而跨 sibling leaf 强校验父节点一致性。异常通常来自损坏文件或 writer 写出了不合法 levels。

4 Columnar Storage Benchmark

论文的 benchmark 不是只用均匀随机数据,而是先分析真实数据集,再抽取可控的数据分布参数。

核心参数包括:

NDV:distinct value 数量。Null ratio:空值比例。Value range:数值范围。Sortedness:排序程度。Skew pattern:是否存在长尾或 heavy hitters。

真实数据的几个特征会直接影响格式表现:

低 NDV 常见:字典编码更有价值。分布通常不均匀:gentle Zipf 或 heavy hitters 会影响 RLE、dictionary 和 zone map。排序程度分化:一些列高度有序,一些列基本无序。整数常有较小值域:bit-packing 等整数编码有用。

这个 benchmark 设计的意义在于隔离变量:先控制数据分布,再分别看 encoding、compression、metadata、index 和 nested model 对文件大小与读取性能的影响。

5 Evaluation

论文的 evaluation 部分覆盖整体文件大小、scan/selection、encoding、block compression、wide table projection、nested data model、ML workloads 和 GPU decoding。下面按这些技术维度整理。

5.1 Overall File Size and Scan Performance

Parquet 和 ORC 没有绝对赢家。

Parquet 通常在列解码上更简单,很多场景下扫描更快。ORC 在一些选择过滤上受益于更细粒度统计信息。文件大小也依赖数据类型、基数、排序程度、skew 和是否启用压缩。

这个结果说明,端到端性能不是单一格式特性决定的。扫描路径中至少有几类成本:

metadata 解析;
column chunk/page 定位;
level/index 解码;
字典或整数编码解码;
块解压;
predicate pruning;
batch materialization。

不同 workload 会放大不同成本。

5.2 Encoding Analysis

Encoding analysis 的核心结论是:真实数据的低 NDV 让字典编码成为很强的默认策略,但编码不能只看压缩率。

整数列、字符串列和浮点列的最优策略不同:

整数列:dictionary + 简单整数编码可能同时带来较小文件和较快解码。字符串列:压缩率和解码速度之间更容易出现 tradeoff。浮点列:传统整数编码不适用,是否做字典或专门 float compression 取决于分布。

对现代系统来说,decode speed 经常比 compression ratio 更关键。格式设计应该尽量避免复杂分支、串行依赖和不利于 SIMD/GPU 的编码。

5.3 Block Compression

块压缩的收益取决于 I/O 节省和解压成本之间的平衡。论文的整体方向是:在现代硬件上,通用块压缩不应无条件默认开启。

这不是说压缩无用,而是压缩应当成为 context-aware 的策略:

本地 SSD + CPU-bound scan:解压成本可能超过 I/O 节省。对象存储:传输量减少可能更重要,但还要看 range request 数。GPU decoding:减少传输量可能有价值,但编码必须适合并行解码。

因此,压缩策略应该和 encoding、storage medium、scan pattern 一起决定。

5.4 Wide Table Projection

宽表投影是论文中对 ML 特征表非常关键的实验。现象是:即使 projected columns 数量固定,随着总列数增加,读取延迟仍可能上升。

原因主要在 metadata:

schema metadata 变大;
chunk metadata 变大;
reader 需要解析或遍历更多列描述;
目标列定位成本上升。

这类问题在超宽表里很明显。训练时可能一次读几百或几千个 feature,即使只读一小部分列,也不希望 reader 解析全量 schema/footer。

更合理的 metadata layout 应该具备:

集中存放:减少对象存储上的小范围随机读。可按列随机访问:允许 reader 通过 column directory 直接定位目标列 metadata。

可以抽象成:

file footer
├── global summary
├── column directory
│   ├── column id/name -> metadata offset
│   ├── column id/name -> page index offset
│   └── column id/name -> statistics offset
└── column metadata blocks├── column A metadata├── column B metadata└── ...

这样 projection 只涉及 col_17, col_998, col_2048 时,reader 不需要完整反序列化所有列 metadata。

除了 metadata,宽表还会放大其他固定成本:

projected column count × row group count × page/stream count

每列都有 page header、encoding、dictionary、null 信息或 level stream。宽投影会放大 per-column reader setup。训练框架还需要把 column vectors 组装成样本 batch,这会产生额外 memory access 和 materialization 成本。

5.5 Auxiliary Data Structures

辅助结构的价值在于用更小的元数据访问换更少的数据扫描。未来格式可以投入更多空间保存更丰富的统计、索引、过滤器或预计算结构。

但设计重点不是“越细越好”,而是:

索引粒度细,便于过滤;
物理布局集中,便于批量读取;
读取计划合并 range,减少 round trip。

对象存储下尤其要避免细粒度 metadata 分散在大量位置。否则剪枝减少的数据读取,可能被 index metadata 自身的 GET 数抵消。

5.6 Nested Data Models

Nested data model 的实验说明,Parquet 和 ORC 的嵌套表达各有代价。

Parquet leaf column 自包含,读少量嵌套 leaf 时比较直接,但共享父节点的结构信息可能在多个 sibling leaf 的 definition levels 中重复。ORC 的 presence/length 更集中,减少结构信息重复,但读取 leaf 时可能需要额外读取结构 stream,并承担更多结构解码或转换成本。

更重要的是磁盘格式和内存格式之间的转换。现代执行引擎经常使用 Arrow-style 内存模型,磁盘上的 nested representation 需要转换成 offsets、validity bitmap、child arrays 等结构。未来格式不仅要考虑磁盘上压得小,还要考虑到内存模型的 translation overhead。

5.7 Machine Learning Workloads

论文把 ML workload 拆成几个场景:vector embeddings、vector search pipeline 和 unstructured data。

5.7.1 Vector Embeddings

Embedding 通常是固定长度 float array。它的关键问题不是传统 SQL 里的列过滤,而是:

float compression;
连续布局;
SIMD/GPU decoding;
batch deserialization;
转换成 NumPy/Arrow/Tensor 等内存格式。

Parquet/ORC 的通用 layout 和压缩策略不是专门为 embedding 设计的。未来格式需要更适合浮点数组和向量化解码的 physical layout。

5.7.2 Vector Search Pipeline

向量检索场景常见路径是:先用 FAISS 或其他向量索引找到 top-k row ids,再回表读取 URL、文本、标签或其他属性。

这个模式考验的是低选择率随机读取和 metadata/index 布局:

vector index search:找到目标 row ids。file selection:根据 row ids 从 Parquet/ORC 回表读取属性列。

本地 SSD 上,细粒度 zone map 可能帮助 ORC 减少读取。对象存储上,如果相关 metadata 分散,ORC 可能产生更多 GET,而 Parquet 更集中的 footer 布局反而更有利。

5.7.3 Unstructured Data

图片、音频、视频等大对象不适合和结构化列混在同一个默认 PAX row group 中。

两类数据的 layout 需求相反:

结构化列:希望 row group 较大,以获得更好压缩和扫描效率。大对象:希望更好的随机定位、异步 I/O 和并行读取。

更合理的结构是逻辑统一、物理分离:

logical table:id, label, scalar_features, embedding, imagephysical layout:structured region:id, label, scalar featuresvector region:fixed-size embeddingsblob region:image/audio/video bytes + offset table

5.8 GPU Decoding

CPU 和 GPU 对格式的偏好不同。CPU 场景中,轻量编码和快速解码通常更重要;GPU 场景中,I/O、PCIe 传输和并行粒度可能成为主瓶颈。

适合 GPU 的格式至少需要:

足够多的可并行数据块;
适合 thread block 内并行解码的编码算法;
较少串行依赖;
更好的数据对齐;
能批量提交解码任务的 metadata。

现有 Parquet/ORC 可以被 GPU reader 支持,但格式本身并不是围绕 GPU decoding 设计的。文件块大小、编码串行依赖和 page/stream 组织都可能限制 GPU 利用率。

6 Lessons for Future Formats

论文的 lessons 可以整理成几个格式设计原则。

第一,字典编码应被积极考虑。真实数据中低 NDV 很常见,dictionary encoding 是有效默认策略,但后续整数 code 的解码速度同样重要。

第二,编码应优先考虑 decode speed,而不是单纯追求 compression ratio。复杂编码如果带来大量分支、串行依赖或不利于 SIMD/GPU,可能得不偿失。

第三,块压缩应是 context-aware 策略。是否压缩取决于存储介质、CPU/GPU 路径、列类型和 workload。

第四,metadata layout 应集中且随机访问友好。宽表需要 column directory,目标是只读取 projected columns 相关 metadata,而不是解析全量 footer/schema。

第五,辅助结构可以更丰富,但必须适合对象存储。更细的 index 需要配合集中存放和 range 合并。

第六,嵌套数据模型应减少 disk-to-memory translation overhead。未来格式需要考虑和 Arrow 等内存格式的亲和性。

第七,ML 数据需要专门 layout。宽特征表、embedding、vector search、大对象都不应该完全复用传统 scan/filter 的布局假设。

第八,GPU decoding 需要足够并行单元和可并行编码。CPU 最优策略不能直接迁移到 GPU。

Column grouping 可以看成这些 lesson 的自然延伸。论文没有把它作为完整方案展开,但宽表和 ML workload 的分析都指向按访问模式组织列:

file / segment
├── group: id_and_label
├── group: user_features
├── group: item_features
├── group: embeddings
└── group: blobs

分组依据应来自访问路径,而不是只看业务命名:

共读频率;
数据类型;
冷热程度;
顺序扫描还是随机访问;
是否需要向量化/GPU 解码;
对象大小。

分组过细会增加 metadata、range request 和跨组 materialization 成本;分组过粗又会回到宽表过读和 metadata 放大的问题。因此 column grouping 本质上是 layout planning 问题。

7 Conclusion

Parquet 和 ORC 仍然是有效的通用列式格式,但它们不是现代 ML 数据湖的终局答案。

如果主要是传统 SQL 分析,少量列投影、大范围扫描,Parquet 和 ORC 都仍然适用。差异主要体现在 encoding、compression、statistics 和执行引擎实现。

如果是对象存储上的过滤查询,metadata 和 index 的物理布局非常关键。细粒度索引只有在不制造过多小 GET 时才有价值。

如果是超宽 ML 特征表,主要问题通常不是单列扫描速度,而是 metadata 解析、reader setup、projection fan-out 和 batch materialization。

如果是嵌套数据,Parquet 的 D/R levels 和 ORC 的 presence/length 没有绝对优劣。Parquet 更适合 leaf projection,ORC 更适合减少共享结构信息重复。实际结果取决于嵌套深度、sibling leaf 数量、null 分布和内存格式转换成本。

如果是 embedding、vector search 或大对象读取,两者都不是理想终点。需要更专门的 physical layout、offset table、集中 metadata、向量友好的压缩和更低的随机读取放大。

因此,真正有用的问题不是“选 Parquet 还是 ORC”,而是当前 workload 的主瓶颈在哪一层,以及现有文件格式是否把这个瓶颈固化在物理布局里。

← 返回列表