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 层面分析。
2 Background and Related Work
列式存储的基本优势来自三个方面:
列裁剪:查询只读需要的列。列压缩:同一列数据类型和分布相近,更容易编码和压缩。向量化处理:连续列值适合 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 的主瓶颈在哪一层,以及现有文件格式是否把这个瓶颈固化在物理布局里。
