土法炼钢兴趣小组的算法知识备份

【数据湖与开放表格式】ORC 文件格式与 Parquet 对照

文章导航

分类入口
databasestorage
标签入口
#orc#parquet#columnar-format#stripe#hive#rle#encoding

目录

Parquet 文件格式深拆 拆完了 lakehouse 里最常见的数据文件格式。但开放表格式(Iceberg 支持 Parquet/ORC/Avro 三种数据文件)里还有一个绕不开的列式格式:ORC(Optimized Row Columnar)。它和 Parquet 几乎同期诞生、目标一致(对象/HDFS 上的列式分析存储),却在物理结构、索引组织和生态上走了不同的路。理解 ORC,既能在 Hive 系存量数据上不踩坑,也能反过来更清楚 Parquet 的取舍为什么是那样。

这篇要回答:

证据锚定 Apache ORC 规范orc.apache.org/specification:File Tail、Stripe、Row Group Index、Run Length Encoding、Column Encodings、Bloom Filters)。实验在本机用 pyarrow 24.0.0(底层 Apache ORC C++ 2.2.2)把同一份 30 万行表写成两种格式,dump 物理布局并对比体积,正文里带 === 的输出均来自实跑。

实验环境:Intel Core i9-12900K,31 GiB 内存,WSL2(Linux 6.6.87.2-microsoft-standard-WSL2,x86_64),Python 3.14.5,pyarrow 24.0.0;ORC 写入端为 Apache ORC C++ 2.2.2(文件里 software_version: ORC C++ 2.2.2writer_version: ORC_135)。数据集与第 2 章一致:30 万行、5 列(id int64 顺序、category 低基数字符串、ts 时间戳、amount double、hi 高基数随机串)。


一、两种格式的共同点先说清

ORC 和 Parquet 不是对立的两种思路,而是同一思路的两种实现。共同点:

所以两者的差异是结构与生态层面的工程取舍,不是「谁是列存、谁不是」。下面逐项拆。


ORC 文件的整体布局(规范《File Tail》《Stripe Information》):

"ORC"                      <- 3 字节 magic(开头)
Stripe 0  ┐
Stripe 1  │  <- 数据区,每个 stripe = index streams + data streams + stripe footer
Stripe 2  ┘
File Footer                <- 整个文件的结构:stripe 列表、types、文件级统计、rowIndexStride
Metadata (stripe stats)    <- 每个 stripe 的列统计(独立于 footer)
Postscript                 <- 不压缩,记录 footer/metadata 长度、压缩算法、版本
<1 字节: Postscript 长度>   <- 文件最后一个字节

实跑 dump 一个 zstd 压缩、强制小 stripe 的 ORC 文件的尾部结构:

=== ORC magic & physical layout (zstd, small stripe to force >1 stripe) ===
head3: b'ORC' (ORC magic)  last_byte(postscript len): 25
file_version: 0.12
software_version: ORC C++ 2.2.2
writer: ORC_CPP  writer_version: ORC_135
compression: ZSTD  compression_block_size: 65536
nrows: 300000
nstripes: 3
row_index_stride: 10000
content_length: 2889511
file_footer_length: 311
file_postscript_length: 25
file_length: 2890213
stripe_statistics_length: 362
nstripe_statistics: 3
schema: id: int64
category: string
ts: timestamp[ns]
amount: double
hi: string

2.1 读 ORC 文件的引导过程

ORC 的尾部比 Parquet 多一层间接,引导过程是(规范《File Tail》):

  1. 读文件最后 1 个字节,得到 postscript 的长度(实跑 25)。
  2. 读 postscript(不压缩、protobuf 编码)。它记录:footerLength(实跑 file_footer_length: 311)、compressionZSTD)、compressionBlockSize65536)、version0.12)、metadataLengthwriterVersionORC_135),以及一个 magic="ORC" 校验。
  3. 据 postscript 给的长度,回读 File Footer(可能被压缩)。footer 含:contentLength(数据区字节,实跑 2889511)、stripes(每个 stripe 的偏移与长度)、types(schema 树)、文件级 statisticsnumberOfRows300000)、rowIndexStride10000)、writer 信息。
  4. 据 footer 里的 stripe 列表去读各 stripe。

对照 Parquet:Parquet 读尾部 8 字节(4 字节 footer 长度 + PAR1)直接定位 FileMetaData一层间接;ORC 读 1 字节 postscript 长度 → postscript → footer,两层间接。ORC 多这一层是因为 postscript 必须不压缩(否则读不出「footer 用什么压缩」这个鸡生蛋问题),于是用一个极小的不压缩 postscript 承载「如何解压 footer」的元信息,footer 本体则可压缩。

2.2 为什么把 stripe 统计单独成段

注意实跑里 stripe_statistics_length: 362file_footer_length: 311 是分开的两段。ORC 把每个 stripe 的列统计放在一个独立的 Metadata 段(紧挨 footer),而不是塞进 footer 主体。原因是访问模式不同:

把两者分开,让「只读结构」的场景不必拉取可能很大的 stripe 统计(stripe 多时这段不小)。这是 ORC 相对 Parquet「footer 一锅端」的一个结构性区别。

flowchart TD
  PS["Postscript (不压缩)<br/>footerLen / compression / version"]
  FOOT["File Footer<br/>stripe 列表 / types / 文件级 stats"]
  META["Metadata 段<br/>每个 stripe 的列统计"]
  ST["Stripes<br/>index + data + stripe footer"]
  PS --> FOOT
  FOOT --> META
  FOOT --> ST

stripe 是 ORC 的核心单位,对应 Parquet 的 row group,但内部组织不同。一个 stripe 分三段(规范《Stripe Information》):

StripeInformation(footer 里每个 stripe 一条)记录 offsetindexLengthdataLengthfooterLengthnumberOfRows

实跑看小 stripe 文件的三个 stripe 行数:

=== per-stripe row counts (read_stripe) ===
  stripe 0: rows=115712
  stripe 1: rows=115712
  stripe 2: rows=68576
  sum: 300000

注意 stripe 的行数不是固定的(115712 / 115712 / 68576):ORC 写入端按字节预算stripe_size,默认 64 MB,本实验强制成 1 MiB 以制造多 stripe)切 stripe——内存里凑够约定大小就 flush 一个 stripe。这与 Parquet 的 row group 类似(也常按字节切)。对照本实验里的 Parquet(row_group_size=100000 显式按行切):

=== Parquet counterpart layout (zstd) for contrast ===
parquet num_row_groups: 3  num_rows: 300000  footer_bytes: 2304  file_bytes: 5341628
  RG0: rows=100000 total_byte_size=4467488
  RG1: rows=100000 total_byte_size=4467888
  RG2: rows=100000 total_byte_size=4464232

3.1 row group index:stripe 内的细粒度跳读

ORC 在每个 stripe 内部,rowIndexStride 行(默认 10000)划一个 “row group”(注意这是 ORC 的术语,和 Parquet 的 row group 完全不是一回事——ORC 的 row group 是 stripe 内的索引粒度,约等于 Parquet 的「一组 data page」)。每个 row group 在 row index stream 里有一条 RowIndexEntry,含两样东西(规范《Row Group Index》):

实跑里 row_index_stride: 10000,确认默认 10000 行一个 row group。

3.2 stream:ORC 怎么把「一列」拆开存

这是 ORC 与 Parquet 在「列怎么落地」上最大的结构差异。Parquet 把一列存成若干 page(dictionary page + data page),page 是自描述的编解码单位。ORC 则把一列拆成多个并行的 stream,每个 stream 只装一种「东西」(规范《Stripe Footer》的 Stream kind):

Stream 类型 装什么 对应概念
PRESENT 布尔 RLE,标记每行该列是否非 null Parquet 的 definition level(可空时)
DATA 主数据(整数 RLE、或字典下标、或浮点原值) Parquet data page 的值
LENGTH 变长类型(string/list)的每个元素长度 Parquet 变长编码里的长度部分
DICTIONARY_DATA 字典内容(字典编码时) Parquet 的 dictionary page
SECONDARY 部分类型的第二组数据(如 decimal scale、timestamp 纳秒) ——

也就是说:ORC 把 null 标记、长度、字典、数据各拆成独立的 stream 平行存放;Parquet 把这些(levels、值、字典)打包进 page 的结构里。ORC 的好处是每种 stream 内部数据高度同质(PRESENT 全是布尔、LENGTH 全是整数),单独编码/压缩更整齐;Parquet 的好处是 page 自描述、page index 跳读规整。两种组织没有绝对优劣,是不同的工程偏好。

flowchart LR
  subgraph ORC["ORC: 一列 = 多个并行 stream"]
    P["PRESENT (null 位图)"]
    D["DATA (值/字典下标)"]
    L["LENGTH (变长长度)"]
    DD["DICTIONARY_DATA"]
  end
  subgraph PARQUET["Parquet: 一列 = 若干 page"]
    DP["Dictionary Page"]
    PG0["Data Page 0 (levels+值)"]
    PG1["Data Page 1"]
  end

3.3 整数编码:ORC 的 RLEv2

ORC 对整数列(包括用作长度、字典下标、以及 int/long/timestamp 的底层存储)用专门的 run-length encoding v2(RLEv2)(规范《Run Length Encoding》),有四种子编码,写入端按数据自动选:

这套整数编码是 ORC 在「整数密集型数据」上常常压得很狠的原因——它对 id、外键、时间戳这类列有针对性的子编码。Parquet 的对应物是 DELTA_BINARY_PACKED 等(见 Parquet 编码 第三节),思路相通但落地不同。


四、实测:同一份数据,两种格式的体积

把第 2 章那张 30 万行、5 列的表,用可比的压缩算法分别写成 Parquet 和 ORC,对比文件体积:

=== same 300000-row table, both formats, comparable codecs ===
  parquet  snappy          9162833 bytes     8948.1 KiB
  parquet  zstd            5341628 bytes     5216.4 KiB
  parquet  none           13401936 bytes    13087.8 KiB
  orc      snappy          4822574 bytes     4709.5 KiB
  orc      zstd            2887910 bytes     2820.2 KiB
  orc      uncompressed    7519718 bytes     7343.5 KiB

这个数据集、这两个写入端版本下:

必须强调边界:这不能推广成「ORC 总比 Parquet 小」。 体积高度依赖:数据形态(整数密集 vs 字符串密集)、写入端选的编码与默认参数、压缩算法、列的排序。换一份字符串密集、低基数的数据,或让 Parquet 写入端对整数列显式开 delta 编码(第 2 章实测 idDELTA_BINARY_PACKED 只要 2393 字节/10 万行),结论可能反转。把这组数读成「在我这台机器、这份数据、这两个库版本下的一次观测」,而不是格式优劣排名。

物理布局对照

维度 Parquet(本实验 zstd) ORC(本实验 zstd 小 stripe)
行批单位 row group(本例 3 个,各 10 万行,按行切) stripe(本例 3 个,行数不等,按字节切)
块内细粒度索引 page + page index(column/offset index) row group index(每 10000 行,positions + stats)
一列的落地 若干 page(dict page + data page) 多个并行 stream(PRESENT/DATA/LENGTH/…)
元数据位置 footer(一段,文件尾) postscript + footer + metadata(三段,文件尾)
footer 大小(本例) 2304 字节 311 字节(+ stripe stats 362 字节)
整数编码 DELTA_BINARY_PACKED / 字典 等 RLEv2(SHORT_REPEAT/DIRECT/PATCHED_BASE/DELTA)
magic 首尾 PAR1 开头 ORC + 尾部 postscript

五、三级统计:ORC 的裁剪漏斗

和 Parquet 一样,ORC 靠多级统计在读数据前裁剪,但分级不同。ORC 是文件 / stripe / row-group 三级(规范《Indexes》):

flowchart LR
  F["文件级统计 (File Footer)<br/>整列 min/max/count,裁整个文件"]
  S["stripe 级统计 (Metadata 段)<br/>裁不相关 stripe"]
  R["row group 级统计 (stripe 内 row index)<br/>每 10000 行,裁 stripe 内部"]
  B["bloom filter (可选)<br/>高基数等值"]
  F --> S --> R
  F --> B

实跑里 nstripe_statistics: 3(三个 stripe 各一份统计)、row_index_stride: 10000 都确认了这套分级。和 Parquet 对照:ORC 的「stripe 统计 + row group index」≈ Parquet 的「row group 统计 + page index」,只是 ORC 把 stripe 统计单独成段(第 2.2 节),Parquet 把 page index 单独成区——两者都意识到「细粒度统计要能独立于主数据被拉取,才能跳读」。

把 ORC 接到 Iceberg 元数据树 的裁剪漏斗里,整条链是:partition pruning(manifest)→ file pruning(manifest 文件级 stats)→ stripe pruning(ORC stripe stats)→ row-group pruning(ORC row index)。Iceberg 不在乎数据文件是 Parquet 还是 ORC——它在 manifest 里存的文件级 stats 一视同仁,只是文件内部那两级裁剪换成了 ORC 的 stripe/row-group。


六、生态与历史:为什么有两个列式格式

ORC 和 Parquet 几乎同期出现(2013 年前后),分属两条社区脉络:

二十年演化下来,生态格局大致是:

维度 Parquet ORC
起源 Twitter/Cloudera,受 Dremel 启发 Hive 社区,继承 RCFile
强势生态 Spark、Trino、DuckDB、Arrow、pandas、ML/AI 工具链 Hive、Spark on Hadoop、Hive ACID 事务表
跨语言库 Arrow(C++/Rust/Java/Python 等)一等支持 主要 Java / C++
lakehouse 表格式默认 Iceberg/Delta/Hudi 默认 Parquet Iceberg 可选 ORC;Delta 基本只用 Parquet
内存格式衔接 Arrow 紧密 衔接不如 Parquet 顺

事实层面:当前 lakehouse 三家表格式都以 Parquet 为默认数据文件(Iceberg 同时支持 ORC/Avro,Delta 基本是 Parquet-only,Hudi 以 Parquet 为 base file)。Parquet 借 Arrow 生态成了跨引擎、跨语言的事实标准,AI/ML 工具链(pandas、polars、各类 dataloader)也几乎只认 Parquet。


七、什么场景仍用 ORC

ORC 不是过时格式,在下面这些场景仍是合理甚至更优的选择:

反过来,新建 lakehouse、需要多引擎(Trino/DuckDB/Spark/Flink)共享、要接 Arrow/AI 工具链、用 Delta 表格式时,默认 Parquet 更省心——这也是为什么本系列后续章节的数据文件默认按 Parquet 讲。选格式的判据不是「谁更先进」,而是「你的引擎栈和数据形态认哪个」。

一个常被忽略的事实:表格式(Iceberg)和数据文件格式(Parquet/ORC)是正交的。同一张 Iceberg 表里甚至可以混有 Parquet 和 ORC 文件——manifest 里每个 data file 都记了 file_format(见 Iceberg 元数据树 第六节的 file_format 字段)。所以「用 Iceberg」和「用 Parquet 还是 ORC」是两个独立决策。


八、小结

ORC 与 Parquet 是同一思路(磁盘列式、行批切分、多级统计裁剪)的两种实现,差异在结构与生态:

下一篇转向另一个维度——数据在内存里的列式格式:Apache Arrow 内存格式与零拷贝,看 Parquet/ORC(磁盘)与 Arrow(内存)如何分工,以及 Arrow 怎么让跨进程、跨语言传数据不发生拷贝。


返回 系列目录

上一篇:Parquet 文件格式深拆

下一篇:Apache Arrow 内存格式与零拷贝


参考资料

  1. Apache ORC, ORCv1 Specificationorc.apache.org/specification/ORCv1)— File Tail(Postscript/Footer)、Stripe Information、Column Encodings、Stream kinds。
  2. Apache ORC, ORCv1 Specification — Run Length Encoding(RLEv1/RLEv2 的 SHORT_REPEAT/DIRECT/PATCHED_BASE/DELTA 子编码)。
  3. Apache ORC, ORCv1 Specification — Indexes / Row Group Index / Bloom Filters(文件/stripe/row-group 三级统计与 row index positions)。
  4. Apache Parquet, apache/parquet-formatparquet.thriftPageIndex.md)— 对照用,见 Parquet 文件格式深拆
  5. Apache Iceberg Table Spec, Data File Fieldsfile_format 字段,表格式与数据文件格式正交)。
  6. 本机实验,2026-06-30:Intel Core i9-12900K / 31 GiB / WSL2(Linux 6.6.87.2-microsoft-standard-WSL2)/ Python 3.14.5 / pyarrow 24.0.0(Apache ORC C++ 2.2.2);30 万行 5 列,ORC 与 Parquet 体积及物理布局均为实跑 dump,体积为单次确定性写出(不涉及计时)。

附录、ORC 工程注记

下面补一组读写 ORC 时常踩或常问的点,每条锚定 ORC 规范或本机实跑。

postscript 为什么必须不压缩

读 ORC 的第一步是读最后 1 字节拿 postscript 长度,再读 postscript 才知道「footer 用什么压缩算法、压了多长」。如果 postscript 自己也被压缩,就陷入「要先知道压缩算法才能解 postscript,可压缩算法写在 postscript 里」的死循环。所以规范规定 postscript 永远不压缩,用一个极小(本实验 25 字节)的明文 protobuf 承载「如何解压其余部分」的引导信息。这是 ORC 比 Parquet 多一层尾部间接的原因。

类型树与列编号

ORC 的 schema 是一棵类型树(types 列表),按先序遍历给每个节点(包括嵌套的 struct/list/map 的子节点)分配一个整数列 id。struct 是根,其字段是子节点。统计、stream 都按这个列 id 索引。这与 Parquet 用「路径 + field id」不同:ORC 的列 id 是位置派生的,schema 演进(增删列)要小心列 id 的对应关系——这也是 Iceberg 这类表格式用自己的 field id 体系、不依赖文件内列 id 的原因之一(见 Iceberg 元数据树)。

压缩块的 3 字节头

ORC 的压缩不是整流一次压,而是compressionBlockSize(本实验 65536 字节)分块压,每个压缩块前有一个 3 字节头:低 1 位是 isOriginal(这一块是否未压缩——当压缩后反而变大时直接存原文),其余位是块长度。分块压缩让读取端能只解压需要的块(配合 row index 的 positions 做 seek),不必从 stream 头解到尾。Parquet 是按 page 压,粒度单位不同但目的一致:让局部读不必解压全列。

column encoding:DIRECT 与 DICTIONARY 的 v1/v2

stripe footer 里每列有一个 ColumnEncoding,kind 取 DIRECTDICTIONARYDIRECT_V2DICTIONARY_V2_V2 表示整数用 RLEv2(带 SHORT_REPEAT/DIRECT/PATCHED_BASE/DELTA 子编码),是现代 ORC 的默认;老文件可能是 v1(RLEv1)。DICTIONARY 表示该列用了字典(字典内容在 DICTIONARY_DATA stream,下标在 DATA stream)。写入端按列的基数自动决定走 DIRECT 还是 DICTIONARY,有一个字典大小阈值(dictionary_key_size_threshold),超过就退回 DIRECT——和 Parquet 的字典回退是同一思路。

Hive ACID 的额外列

Hive 的事务表(ACID v2)在 ORC 文件里给每行加一组隐藏列:operationoriginalTransactionbucketrowIdcurrentTransaction,用来表达行级 insert/update/delete 与可见性。这套机制深度绑定 ORC 的结构,是「Hive 行级事务历史上要求 ORC」的根。lakehouse 的开放表格式不用这套,而是用各自的 delete file / deletion vector(第 10 章「行级删除与 Merge-on-Read」展开)在表格式层做行级删除,与文件格式解耦。

SearchArgument:ORC 的谓词下推接口

ORC reader 的谓词下推通过 SearchArgument(SARG)表达:把 WHERE 条件构造成 SARG,reader 用它逐级比对 stripe stats、row group stats、bloom filter,决定跳过哪些 stripe / row group。引擎(Hive/Spark)负责把 SQL 谓词翻译成 SARG。和 Parquet 的下推目的一致,接口形态不同。

writer version 与兼容

实跑里 file_version: 0.12(ORC 文件格式版本)、writer_version: ORC_135(写入端版本枚举)、software_version: ORC C++ 2.2.2(具体库版本)。读取端按 writerVersion 判断要不要绕过某些历史 writer 的已知 bug(规范维护了一张 writer version 与修复对应的表)。跨版本读写以文件格式版本 + writer version 为准,不以单一库版本为准。

常见问题

Q:ORC 和 Parquet,嵌套数据谁更好? 两者都支持 struct/list/map 嵌套。Parquet 用 Dremel 的 repetition/definition level(见 Parquet 第四节);ORC 用类型树 + 每列的 PRESENT stream 表达存在性,list/map 用 LENGTH stream 表达长度。没有一刀切的优劣,取决于引擎对哪种的读取实现更成熟。生态上 Arrow/DuckDB 等对 Parquet 嵌套的支持更广。

Q:同一张 Iceberg 表能混 ORC 和 Parquet 吗? 能。Iceberg manifest 里每个 data file 记 file_format,planning 与读取按文件各自的格式处理。但实践中很少故意混用,多是迁移过程中的过渡态。Delta 基本只认 Parquet。

Q:DuckDB / 新引擎读 ORC 顺手吗? Parquet 在 Arrow 生态、DuckDB、polars、ML 工具链里是一等公民;ORC 的跨引擎支持面窄于 Parquet(主要 Hive/Spark/Java、C++)。新建、需要多引擎或接 AI 工具链的湖,默认 Parquet 摩擦更小。这也是本系列后续默认按 Parquet 讲数据文件的原因。

Q:stripe 应该多大? 默认 64 MB。太大则单 stripe 读放大、内存压力大、stripe 级裁剪粒度粗;太小则 stripe 多、footer/metadata 膨胀、字典与压缩效果差(本实验为演示多 stripe 强制成 1 MiB,生产不会这么小)。和 Parquet row group 一样,要按引擎并行度与存储特性调,并配合按过滤列排序让 stripe 间统计不重叠。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-30 · database / storage

【数据湖与开放表格式】Parquet 文件格式深拆

拆 Parquet 的物理结构:file → row group → column chunk → page,footer 里的 FileMetaData(Thrift)与 PAR1 magic。讲清 PLAIN/RLE-bitpacking/字典/DELTA_BINARY_PACKED/BYTE_STREAM_SPLIT 各自压谁,Dremel 的 repetition/definition level 如何表达嵌套,column index/offset index 与 split-block bloom filter 怎样让谓词在读盘前裁掉 page。基于本机 pyarrow 24.0.0 真实 dump footer 与编码。

2026-06-30 · database / storage

【数据湖与开放表格式】列式编码与压缩

拆解 Parquet 的两层缩减:专用编码(dictionary / RLE / DELTA_BINARY_PACKED / BYTE_STREAM_SPLIT)降熵,再用 zstd/snappy/lz4/gzip 压字节。用 pyarrow 在同一列上实测不同编码+压缩组合的体积与读取耗时(3M 行,7 轮中位数),并与 ClickHouse CODEC 做同思想不同落地的对照。

2026-06-30 · database / storage

【数据湖与开放表格式】Lakehouse 全景:从 Hive 表到开放表格式

Hive 目录式分区表把『表』等同于『一组目录加 metastore 里的分区行』,于是没有原子提交、planning 要 LIST 目录、schema 与分区演进常要重写。本文用这三个硬伤切入,讲清 lakehouse 把表拆成『不可变数据文件 + 可变元数据指针 + catalog』三层后各自解决了什么,并给出全系列的分层地图。

2026-06-30 · database / storage

【数据湖与开放表格式】Apache Arrow 内存格式与零拷贝

拆解 Arrow 列式内存布局(validity bitmap + value buffer + offset buffer)、零拷贝从何而来,以及 C Data Interface、IPC、Flight 三层跨边界传递。讲清 Arrow(内存计算格式)与 Parquet(磁盘存储格式)如何分工衔接。含 pyarrow 实测 C Data Interface 同地址零拷贝。


By .