Parquet 文件格式深拆 拆完了 lakehouse 里最常见的数据文件格式。但开放表格式(Iceberg 支持 Parquet/ORC/Avro 三种数据文件)里还有一个绕不开的列式格式:ORC(Optimized Row Columnar)。它和 Parquet 几乎同期诞生、目标一致(对象/HDFS 上的列式分析存储),却在物理结构、索引组织和生态上走了不同的路。理解 ORC,既能在 Hive 系存量数据上不踩坑,也能反过来更清楚 Parquet 的取舍为什么是那样。
这篇要回答:
- ORC 的文件结构(postscript + footer + stripe)和 Parquet(footer + row group)差在哪?为什么 ORC 把元数据切成两段放尾部?
- ORC 的「stripe / row group index / stream」三个概念,分别对应 Parquet 的什么?
- 同一份数据写成 ORC 和 Parquet,体积差多少、物理布局差多少?(本机实测)
- 什么场景今天仍该用 ORC?
证据锚定 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.2、writer_version: ORC_135)。数据集与第 2 章一致:30 万行、5 列(idint64 顺序、category低基数字符串、ts时间戳、amountdouble、hi高基数随机串)。
一、两种格式的共同点先说清
ORC 和 Parquet 不是对立的两种思路,而是同一思路的两种实现。共同点:
- 都是磁盘上的列式格式:同一列的数据连续存放,按列读取,利于压缩和投影下推。
- 都做行批切分:先把表按行切成大块(ORC 叫 stripe,Parquet 叫 row group),块内再按列组织。这是为了在「纯列存(全列分开)」和「可并行、可裁剪」之间取平衡。
- 都有多级统计做谓词下推:min/max 等统计让查询在读数据前跳过不相关的块。
- 都支持嵌套类型、字典编码、块压缩(zstd/snappy/zlib 等)、bloom filter。
- 都是不可变文件:写完不改,契合对象存储语义(见 对象存储语义与代价),也契合 lakehouse 表格式「数据文件不可变」的前提。
所以两者的差异是结构与生态层面的工程取舍,不是「谁是列存、谁不是」。下面逐项拆。
二、ORC 的文件结构:postscript + footer + stripe
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 个字节,得到
postscript 的长度(实跑
25)。 - 读 postscript(不压缩、protobuf
编码)。它记录:
footerLength(实跑file_footer_length: 311)、compression(ZSTD)、compressionBlockSize(65536)、version(0.12)、metadataLength、writerVersion(ORC_135),以及一个magic="ORC"校验。 - 据 postscript 给的长度,回读 File
Footer(可能被压缩)。footer
含:
contentLength(数据区字节,实跑2889511)、stripes(每个 stripe 的偏移与长度)、types(schema 树)、文件级statistics、numberOfRows(300000)、rowIndexStride(10000)、writer 信息。 - 据 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: 362 与
file_footer_length: 311 是分开的两段。ORC
把每个 stripe 的列统计放在一个独立的
Metadata 段(紧挨 footer),而不是塞进 footer
主体。原因是访问模式不同:
- 只想知道「这张表多大、有哪些列」→ 读 footer 就够,不必加载所有 stripe 的统计。
- 要做谓词下推、裁剪 stripe → 才需要 stripe statistics 段。
把两者分开,让「只读结构」的场景不必拉取可能很大的 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 内部:index / data / stripe footer 三段
stripe 是 ORC 的核心单位,对应 Parquet 的 row group,但内部组织不同。一个 stripe 分三段(规范《Stripe Information》):
- Index Streams:该 stripe 内各列的 row index(行组索引)和可选的 bloom filter。
- Data Streams:该 stripe 内各列的实际数据,按 stream 类型拆分(见 3.2)。
- Stripe Footer:该 stripe 的 stream 目录(每个 stream 的类型、所属列、长度)和各列的 column encoding。
StripeInformation(footer 里每个 stripe
一条)记录
offset、indexLength、dataLength、footerLength、numberOfRows。
实跑看小 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》):
- positions:该 row group 在各 data stream 里的起始位置(压缩块偏移 + 块内偏移 + RLE 内偏移),让读取端能 seek 到 stripe 中段直接解某个 row group,不必从 stripe 头解到尾。
- statistics:该 row group 的列统计(min/max/null/sum 等),做 stripe 内的细粒度谓词下推。
实跑里 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》),有四种子编码,写入端按数据自动选:
- SHORT_REPEAT:少量重复值(如连续相同)。
- DIRECT:固定位宽打包(杂乱值)。
- PATCHED_BASE:大部分值接近某基准、少量离群值用「补丁」单独记,避免离群值撑大整体位宽。
- DELTA:单调或固定步长序列,存基准 +
差分(对
id=0,1,2,...这种极有效)。
这套整数编码是 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
在这个数据集、这两个写入端版本下:
- 同为 zstd,ORC(2.82 MiB)比 Parquet(5.22 MiB)小不少;同为 snappy 也是(4.71 vs 8.95 MiB);连不压缩时 ORC(7.34 MiB)也小于 Parquet(13.1 MiB)。
- 差距主要来自整数/时间戳列:本数据集的
id(严格自增)、ts(固定步长递增)对 ORC 的 RLEv2 DELTA 子编码极其友好,而 pyarrow 写 Parquet 时这两列默认走的是字典编码(见第 2 章实跑id的uncomp_sz=1002880),没用上 delta,所以 Parquet 这两列偏大。
必须强调边界:这不能推广成「ORC 总比 Parquet
小」。 体积高度依赖:数据形态(整数密集 vs
字符串密集)、写入端选的编码与默认参数、压缩算法、列的排序。换一份字符串密集、低基数的数据,或让
Parquet 写入端对整数列显式开 delta 编码(第 2 章实测
id 用 DELTA_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
- 文件级:footer 里每列的统计,回答「这个文件值不值得打开」。
- stripe 级:Metadata 段里每个 stripe 每列的统计,裁掉不相关的 stripe(类似 Parquet 的 row group 统计)。
- row group 级:stripe 内 row index 里每 10000 行一条统计 + positions,既能裁掉 stripe 内的行段,又能 seek 到中间直接解(类似 Parquet 的 page index)。
- bloom filter:可选的
BLOOM_FILTER/BLOOM_FILTER_UTF8stream,按 row group 粒度,给高基数等值谓词用(思路同 Parquet,见 Parquet bloom filter 第五节)。
实跑里 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 年前后),分属两条社区脉络:
- ORC 出自 Apache Hive 社区,是 RCFile 的继任者,设计上紧贴 Hive 的执行模型与 ACID 需求。它在 Hive / Hadoop 栈里是一等公民:Hive 的事务表(ACID,行级 update/delete)历史上要求 ORC 格式(依赖 ORC 的 row-level 结构与 ACID 列)。
- Parquet 出自 Twitter 与 Cloudera,受 Google Dremel 论文启发(repetition/definition level 直接来自 Dremel,见 Parquet 第四节)。它从一开始就定位为「跨引擎、跨语言的通用列式格式」。
二十年演化下来,生态格局大致是:
| 维度 | 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 不是过时格式,在下面这些场景仍是合理甚至更优的选择:
- Hive 事务表(ACID):Hive 的行级 update/delete 事务表历史上绑定 ORC 的结构。维护存量 Hive ACID 表,就得用 ORC。
- Hive / Spark on Hadoop 存量栈:已有大量 ORC 数据、工具链围绕 Hive 建立,没有跨引擎需求时,迁移成本不划算。
- 整数/时间戳密集、对压缩比敏感:RLEv2 的多子编码对这类列常有很好的压缩比(本章实测在该数据集上 ORC 体积明显更小),存储成本敏感且数据形态匹配时值得评估。
- 强 Hive 元数据集成:ORC 的某些 Hive 专有元数据(如 ACID 相关列)在 Hive 生态里有原生支持。
反过来,新建 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 是同一思路(磁盘列式、行批切分、多级统计裁剪)的两种实现,差异在结构与生态:
- 文件结构:ORC 是
ORCmagic + stripe + footer + metadata + postscript(尾部两层间接,postscript 不压缩以承载「footer 怎么解压」);Parquet 是PAR1+ row group + footer(一层间接)。实测 ORC footer 311 字节 + stripe stats 362 字节,Parquet footer 2304 字节。 - 块与列的组织:ORC 用 stripe(按字节切,本例 115712/115712/68576 行)+ stripe 内每 10000 行的 row group index;一列拆成 PRESENT/DATA/LENGTH/DICTIONARY_DATA 等并行 stream。Parquet 用 row group + page + page index;一列是若干 page。
- 整数编码:ORC 的
RLEv2(SHORT_REPEAT/DIRECT/PATCHED_BASE/DELTA)对整数/时间戳密集列常压得很狠——本机实测同数据
zstd 下 ORC 2.82 MiB vs Parquet 5.22 MiB,主因是
id/ts在 ORC 走 delta、在 pyarrow 默认 Parquet 走字典。此结论限本数据集与库版本,不能推广为格式排名。 - 三级统计:ORC 是文件 / stripe / row-group 三级 + 可选 bloom;对应 Parquet 的 chunk 统计 / page index / bloom。接进 Iceberg 裁剪漏斗时,文件内那两级换成 ORC 的 stripe/row-group。
- 生态:Parquet 借 Arrow 成跨引擎跨语言事实标准,lakehouse 默认 Parquet;ORC 强在 Hive 系与 ACID 事务表。表格式与文件格式正交,「用 Iceberg」不绑定「用 Parquet 还是 ORC」。
下一篇转向另一个维度——数据在内存里的列式格式:Apache Arrow 内存格式与零拷贝,看 Parquet/ORC(磁盘)与 Arrow(内存)如何分工,以及 Arrow 怎么让跨进程、跨语言传数据不发生拷贝。
返回 系列目录
上一篇:Parquet 文件格式深拆
参考资料
- Apache ORC, ORCv1 Specification(orc.apache.org/specification/ORCv1)— File Tail(Postscript/Footer)、Stripe Information、Column Encodings、Stream kinds。
- Apache ORC, ORCv1 Specification — Run Length Encoding(RLEv1/RLEv2 的 SHORT_REPEAT/DIRECT/PATCHED_BASE/DELTA 子编码)。
- Apache ORC, ORCv1 Specification — Indexes / Row Group Index / Bloom Filters(文件/stripe/row-group 三级统计与 row index positions)。
- Apache Parquet,
apache/parquet-format(parquet.thrift、PageIndex.md)— 对照用,见 Parquet 文件格式深拆。 - Apache Iceberg Table Spec, Data File
Fields(
file_format字段,表格式与数据文件格式正交)。 - 本机实验,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 取
DIRECT、DICTIONARY、DIRECT_V2、DICTIONARY_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
文件里给每行加一组隐藏列:operation、originalTransaction、bucket、rowId、currentTransaction,用来表达行级
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 间统计不重叠。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据湖与开放表格式】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 与编码。
【数据湖与开放表格式】列式编码与压缩
拆解 Parquet 的两层缩减:专用编码(dictionary / RLE / DELTA_BINARY_PACKED / BYTE_STREAM_SPLIT)降熵,再用 zstd/snappy/lz4/gzip 压字节。用 pyarrow 在同一列上实测不同编码+压缩组合的体积与读取耗时(3M 行,7 轮中位数),并与 ClickHouse CODEC 做同思想不同落地的对照。
【数据湖与开放表格式】Lakehouse 全景:从 Hive 表到开放表格式
Hive 目录式分区表把『表』等同于『一组目录加 metastore 里的分区行』,于是没有原子提交、planning 要 LIST 目录、schema 与分区演进常要重写。本文用这三个硬伤切入,讲清 lakehouse 把表拆成『不可变数据文件 + 可变元数据指针 + catalog』三层后各自解决了什么,并给出全系列的分层地图。
【数据湖与开放表格式】Apache Arrow 内存格式与零拷贝
拆解 Arrow 列式内存布局(validity bitmap + value buffer + offset buffer)、零拷贝从何而来,以及 C Data Interface、IPC、Flight 三层跨边界传递。讲清 Arrow(内存计算格式)与 Parquet(磁盘存储格式)如何分工衔接。含 pyarrow 实测 C Data Interface 同地址零拷贝。