在 Iceberg 元数据树 里,我们看到每个 data file 的分区元组直接存在 manifest 里,planner 靠它做 partition pruning。这一篇回答两个 Hive 时代长期烧人的问题:
- 用户查询写的是
WHERE ts >= '2026-01-02',但表是按天分区的。Hive 里你得自己写WHERE dt = '2026-01-02',写错或忘写就全表扫。Iceberg 怎么让查询只写数据列谓词、由系统自动裁分区?这就是隐藏分区(hidden partitioning)。 - 表按天分区跑了一年,发现某些天数据量暴涨,想改成「天 + 按 category 分桶」。Hive 里改分区方案基本等于重建表、重写历史。Iceberg 怎么做到改了分区方案却不重写一行老数据?这就是分区演进(partition evolution)。
证据锚定 Apache Iceberg 表规范
的《Partitioning》《Partition Transforms》《Partition
Evolution》《Scan Planning》各节,与
apache/iceberg 1.x 的
org.apache.iceberg.transforms.*。实验沿用第 8
篇那张 sales.orders
表,在它上面真实演进 spec 并追加数据,带
== 的输出均来自实跑。
实验环境:Intel Core i9-12900K,32 GB 内存,Linux 6.6(WSL2,Arch),Python 3.14.5,pyiceberg 0.11.1,pyarrow 24.0.0;catalog=SQLite,warehouse=本地 FS,
format-version=2。
一、Hive 分区的「分区裂脑」
Hive
把分区做成目录:/table/dt=2026-01-02/...。这套模型有三个反复咬人的问题:
- 分区列是一份独立的物理列。
dt通常是从ts派生出来、再单独写进去的列。源列ts和分区列dt是两份数据,靠写入端自觉保持一致——一旦写入逻辑算错时区、跨天边界处理不一致,dt和ts就对不上,查询按dt裁到的文件里混着别的天。这种「同一语义两份数据各说各话」可以叫分区裂脑。 - 查询必须显式写分区谓词。优化器一般不知道
dt = func(ts)这个隐含关系,用户写WHERE ts >= '2026-01-02'而不写dt,引擎只能全表扫;写了dt又得保证和ts谓词语义一致。负担全压在写 SQL 的人身上。 - 分区方案几乎不可演进。目录结构写死了分区键。想从「按天」改成「按小时」或「天 + 分桶」,老目录布局变不了,通常只能重建表、重写历史数据。
Iceberg 的设计目标之一(表规范《Goals》)直接对着这三点:「Partitioning will be table configuration. Reads will be planned using predicates on data values, not partition values.」——分区是表的配置,读用数据值谓词来 plan,而非分区值。下面看它怎么落地。
二、partition spec 与 transform
partition spec 的结构
表规范《Partitioning》定义:一张表配一个 partition spec,spec 是一组 partition field,每个 field 由四部分构成:
- 一个(或一组,V3 多参数)源列 id;
- 一个在 spec 内唯一的 partition field id(V2 起跨所有 spec 唯一);
- 一个作用在源列上的 transform;
- 一个 partition 名字。
关键点:spec 存的是「从表数据到分区值的变换」,不只是分区值本身。规范明确这份变换被用来把数据列谓词转成分区谓词——这正是隐藏分区与跨 spec 裁剪能成立的根。源列必须是基本类型(可嵌在 struct 里,但不能在 map/list 内)。
第 8 篇建表时用的 spec 0(实跑 dump):
spec-id 0 [(field_id=1000, name='ts_day', transform='day', source_id=2)]
含义:对源列 ts(id=2)施加 day
变换,产出名为 ts_day、partition field id=1000
的分区字段。
内置 transform
表规范《Partition Transforms》给出全部内置变换(源类型与结果类型摘录):
| transform | 含义 | 源类型(摘录) | 结果类型 |
|---|---|---|---|
identity |
原值不变 | 除 geometry/geography/variant 外任意 | 源类型 |
bucket[N] |
哈希后对 N 取模 | int/long/decimal/date/time/timestamp(tz)/string/uuid/fixed/binary | int |
truncate[W] |
截断到宽度 W | int/long/decimal/string/binary | 源类型 |
year |
取年(自 1970 起的年数) | date/timestamp(tz)/timestamp_ns | int |
month |
取月(自 1970-01 起的月数) | date/timestamp(tz)/timestamp_ns | int |
day |
取日(自 1970-01-01 起的天数) | date/timestamp(tz)/timestamp_ns | date |
hour |
取小时(自 1970-01-01 00:00 起的小时数) | timestamp(tz)/timestamp_ns | int |
void |
恒为 null | 任意 | 源类型或 int |
几条要点:
- 所有 transform 对
null输入返回null(规范明确)。 day结果类型是date,但读端必须也接受int(把整数当作「自 1970-01-01 起的天数」对应的日期)——这解释了第 8 篇 scan plan 里分区值是整数20455/20456:2026-01-02、2026-01-03 距 1970-01-01 的天数。void不是删分区,而是把某个分区字段的 transform 换成「恒 null」,在 V1 里用来「逻辑上丢弃」一个分区字段(见第五节演进规则)。
bucket 与 truncate 的细节
bucket[N](表规范《Bucket Transform Details》):用 32 位 Murmur3 哈希(x86 变体,seed=0),先丢掉符号位再对 N 取模:
\[ \text{bucket}_N(x) = \big(\,\text{murmur3\_x86\_32}(x)\ \&\ \texttt{Integer.MAX\_VALUE}\,\big)\ \%\ N \]
& Integer.MAX_VALUE(即
& 0x7fffffff)保证结果非负。哈希按类型的字节编码细节见规范
Appendix B;对应源码
org.apache.iceberg.transforms.Bucket(apache/iceberg
1.x)。bucket 适合高基数列(如用户 id)做均匀打散,避免「按
identity 分区导致海量小分区」。
truncate[W](表规范《Truncate Transform Details》)按类型不同:
| 类型 | 规则 | 例 |
|---|---|---|
int/long |
\(v - (v \bmod W)\),余数取正 | W=10:1→0,-1→-10 |
decimal |
按列 scale 缩放 W 后同上 | W=50, s=2:10.65→10.50 |
string |
取前 L 个码点:v.substring(0, L) |
L=3:iceberg→ice |
binary |
取前 L 字节 | L=3:\x01\x02\x03\x04\x05→\x01\x02\x03 |
整型 truncate 的余数必须取正,规范给了在 %
可能返回负值的语言里的正确写法 \(v
- (((v \bmod W) + W) \bmod
W)\)。truncate
适合「按前缀分区」,例如按字符串前缀或数值区间。
三、隐藏分区:查询不写分区列也能裁剪
「隐藏」体现在两处:分区字段不是用户要手填的独立列,存储布局对查询透明;查询只在源数据列上写谓词,Iceberg 自动推导分区谓词。
机制是表规范《Scan Planning》的核心一句:scan
谓词被转换成分区谓词来筛文件,转换用「写该 manifest 的
partition spec」。具体到
transform,叫包含投影(inclusive
projection):把数据列谓词投影成一个不漏匹配文件的分区谓词。例如
day
是单调的,ts >= 2026-01-02 00:00:00
可安全投影成
ts_day >= day(2026-01-02) = 20455。
第 8 篇那次实跑已经验证:只在 ts
上加谓词,planner 精确剔除了 2026-01-01 的文件:
== scan with hidden-partition predicate (ts only) ==
planned data files for ts>=2026-01-02: 4 # 全表 5 个,剔除了 2026-01-01 那个
00000-0-c9fb….parquet spec 1 Record[20456, 2]
00000-1-c9fb….parquet spec 1 Record[20456, 0]
00000-0-0c18….parquet spec 0 Record[20455]
00000-0-cc2d….parquet spec 0 Record[20455]
我们没有写
ts_day,分区列对查询是隐藏的;裁剪却照常发生。这就消解了
Hive
的两个痛点:分区列不再是用户维护的第二份数据(裂脑没了),查询也不必显式写分区谓词(写错/忘写导致全表扫的坑没了)。
不是所有谓词都能投影。投影的有效性取决于 transform 的单调性与谓词形态:
day(ts)、truncate、year/month/hour等保序变换,对范围谓词(>=、<、BETWEEN)能投影出范围分区谓词;bucket[N]不保序,只能投影等值谓词:category = 'book'投影成category_bucket = bucket4('book'),但category > 'book'无法变成 bucket 上的范围谓词,对 bucket 维度退化为不裁剪;- 谓词里若对源列再套函数(如
WHERE date(ts) = ...用了引擎自己的函数而非分区 transform),能否裁取决于引擎是否识别该等价关系。
所以隐藏分区不是「任何写法都自动最优」,而是「在源列上按 transform 语义写谓词,就能拿到分区裁剪」。
四、实验:演进 partition spec
现在给 sales.orders 演进分区方案:在原有
day(ts) 基础上追加
bucket[4](category)。pyiceberg 实跑:
from pyiceberg.transforms import BucketTransform
with tbl.update_spec() as us:
us.add_field("category", BucketTransform(4), "category_bucket")演进后 dump 所有 spec(实跑):
== partition specs after evolution ==
spec-id 0 [(1000, 'ts_day', 'day', src=2)]
spec-id 1 [(1000, 'ts_day', 'day', src=2), (1001, 'category_bucket', 'bucket[4]', src=3)]
default-spec-id 1
对照表规范《Partition Evolution》:
- 改 spec 产生一个带新 spec id 的新
spec,加入表元数据的
partition-specs列表,并可设为默认;旧 spec(id=0)保留。 - 原有的
ts_day字段 partition field id 仍是 1000 没变;新增的category_bucket分配到 1001(基于last-partition-id递增)。规范要求演进时不要让已有 partition field id 改变,因为这些 id 被用作 manifest 里分区元组的字段 id。 - 演进只改元数据,不碰任何已有数据文件。
接着在演进后的 spec 1 下再追加一批(id 6/7,日期 2026-01-03)。看落盘的目录布局(实跑):
== data files on disk ==
/sales/orders/data/ts_day=2026-01-01/00000-0-dffc….parquet # spec 0
/sales/orders/data/ts_day=2026-01-02/00000-0-cc2d….parquet # spec 0
/sales/orders/data/ts_day=2026-01-02/00000-0-0c18….parquet # spec 0
/sales/orders/data/ts_day=2026-01-03/category_bucket=0/00000-1-8278….parquet # spec 1
/sales/orders/data/ts_day=2026-01-03/category_bucket=2/00000-0-8278….parquet # spec 1
布局直接体现演进:spec 0 时期的文件只有
ts_day=.../,spec 1 之后的文件多了一层
category_bucket=.../。两种布局并存于同一张表,老文件原地不动。
这里的目录名(
ts_day=2026-01-03)只是 pyiceberg 写文件时的可读路径约定,不是 Iceberg 判断分区的依据。Iceberg 认的是 manifest 里记录的分区元组(第 8 篇),即便目录名乱了也不影响 planning。这与 Hive「目录名即分区」是本质区别。
五、分区演进为何不重写历史
核心机制:每个文件携带它所属的 spec,planning 时按文件各自的 spec 转换谓词。
第 8 篇的 inspect.entries
已显示:老文件(seq 1–3)的分区元组是
{ts_day, category_bucket: None},新文件(seq
4)是 {ts_day, category_bucket: 实值}。再看
manifest list(实跑),partition_spec_id 把新旧
manifest 分得清清楚楚:
== inspect.manifests ==
spec_id added partition_summaries
1 2 [{low:2026-01-03, up:2026-01-03}, {low:0, up:2}] # 新 manifest:两个分区字段摘要
0 1 [{low:2026-01-02, up:2026-01-02}] # 老 manifest:只有一个
0 1 [{low:2026-01-02, up:2026-01-02}]
0 1 [{low:2026-01-01, up:2026-01-01}]
为什么必须「一个 manifest 只装一个 spec
的文件」?表规范《Manifests》给了原因:manifest
文件的 schema 由它的 partition spec
决定(分区元组的结构跟着 spec 走),所以 spec
变了就必须写新 manifest,老文件留在老 manifest 里。manifest
list 用 partition_spec_id 标明每个 manifest
用哪个 spec。
planning 时(表规范《Scan Planning》):「Conversion uses the partition spec that was used to write the manifest file regardless of the current partition spec.」——用写该 manifest 的 spec,而非当前 spec 来把数据谓词转成分区谓词。于是:
- 对 spec 0 的老
manifest:
ts >= 2026-01-02投影成ts_day >= 20455,category_bucket维度不存在、不参与; - 对 spec 1 的新 manifest:同一个谓词同样投影
ts_day,若再有category = 'book'还能在新文件上投影category_bucket。
这就是第 8 篇 scan plan 里 spec 0 与 spec 1 文件混在一起被正确裁剪的原因。整个过程没有重写任何老数据:演进的代价只是表元数据多一个 spec、之后的新文件走新布局。
flowchart TD
P["查询谓词(源列)<br/>ts >= 2026-01-02"]
P --> S0["对 spec 0 的 manifest<br/>投影 → ts_day >= 20455"]
P --> S1["对 spec 1 的 manifest<br/>投影 → ts_day >= 20455"]
S0 --> F0["裁出 spec 0 老文件"]
S1 --> F1["裁出 spec 1 新文件"]
F0 --> R["候选文件集合(新旧并存)"]
F1 --> R
演进的代价与边界:
- 演进只改元数据,历史数据不重写;但老数据仍按老分区布局存放,新分区方案只对新写入生效。想让老数据也享受新布局,需要单独 rewrite/compaction(第 17 篇)。
- V1 表对演进有额外限制(表规范《Partition
Evolution》):分区 field id 当年是从 1000
顺序分配、未显式跟踪,跨 spec 同 id 可能类型不同,所以 V1
推荐「不重排、不删(改用
void)、只在末尾加」。V2 起每个 partition field id 被显式跟踪,演进更自由。 - transform 本身不能「就地改语义」。把
day改成hour是新增/替换分区字段、产生新 spec,不是修改老字段。
六、与 Hive 静/动态分区对比
| 维度 | Hive 静态/动态分区 | Iceberg 隐藏分区 |
|---|---|---|
| 分区列来源 | 用户显式声明并写入的独立物理列 | 由 transform 从源列派生,不需用户单独维护 |
| 查询谓词 | 必须写分区列谓词才裁剪 | 在源列写谓词,自动投影成分区谓词 |
| 分区识别 | 靠目录名(dt=...) |
靠 manifest 里的分区元组,与目录名解耦 |
| 一致性风险 | 源列与分区列可能不一致(裂脑) | 分区值由 transform 确定,无第二份数据 |
| 分区演进 | 基本要重建表/重写历史 | 加新 spec,老文件原地保留,按各自 spec 裁剪 |
| planning | 递归 LIST 目录 |
读 manifest(O(1) 量级远程调用) |
| 动态分区写入 | 需
INSERT ... PARTITION、易产生大量小分区 |
写入端只写数据,分区由 spec 决定 |
要点不在「Iceberg 全面更好」,而在两者把同一职责放在了不同位置:Hive 把分区当成用户负责的物理列与目录约定,Iceberg 把分区当成表配置 + 元数据派生。前者把一致性与裁剪正确性的负担压给写 SQL 和写 ETL 的人,后者把它收进表格式自己。
七、边界与坑
- bucket
数不能随便改。
bucket[N]的 N 写进 spec;想从bucket[4]改bucket[8]是分区演进(新 spec),老文件仍按 N=4 的桶分布,新老桶号不可比。规划分桶数时要按数据增长留余量。 - 过细分区仍会产生小文件。隐藏分区不改变「分区粒度过细
→ 每分区文件多而小」的物理事实。
day+ 高基数bucket组合若桶数过大,照样制造小文件,需 compaction(第 17 篇)。 - 谓词写法影响是否裁剪。在源列上按 transform 语义写谓词才有投影;对源列套了引擎不识别的函数、或对 bucket 维度写范围谓词,都会让该维度退化为不裁剪。
- 演进不回填老数据。新 spec 只管新写入;老数据要享受新布局得显式 rewrite,且 rewrite 会产生新 snapshot(影响快照保留与存储)。
- 目录名不可信。人肉看目录名判断分区在
Iceberg 里会误导——以
inspect.entries/inspect.partitions的分区元组为准。
实跑 inspect.partitions(含 spec
id,印证「分区按各自 spec 归属」):
== inspect.partitions ==
partition spec_id record_count file_count
{ts_day:2026-01-03, category_bucket:2} 1 1 1
{ts_day:2026-01-03, category_bucket:0} 1 1 1
{ts_day:2026-01-02, category_bucket:None} 0 3 2
{ts_day:2026-01-01, category_bucket:None} 0 2 1
category_bucket: None 的两行是 spec 0
老分区,spec id 标为 0;spec 1
新分区两个字段都有值。同一张表里新旧分区方案并存、各按自己的
spec 统计。
八、transform 选型与写入布局
分区设计的目标是「让常见查询能裁掉大部分文件,同时不制造海量小文件」。不同 transform 适配不同的列与查询形态。
按 transform 的适用场景
| transform | 典型源列 | 适配查询 | 风险 |
|---|---|---|---|
identity |
低基数枚举(地区、租户少时) | 等值/IN | 高基数列会炸出海量分区 |
day/hour |
时间戳 | 时间范围(保序裁剪) | 粒度过细 → 小文件;过粗 → 裁不动 |
month/year |
时间戳 | 长跨度时间范围 | 单分区过大 |
bucket[N] |
高基数 id(user_id、order_id) | 等值点查、按桶 join | 不保序,范围谓词不裁;N 难改 |
truncate[W] |
字符串前缀、数值区间 | 前缀/区间查询 | W 选得不当裁剪弱 |
void |
演进时「逻辑丢弃」字段 | —— | 仅用于演进,不产生新裁剪 |
常见组合是「时间 transform + 高基数
bucket」:day(ts)
控制时间裁剪,bucket[N](user_id)
把每天的数据均匀打散到 N 个桶,既能按时间裁,又能按 user_id
等值裁,还避免单分区过大。本文实验的
day(ts) + bucket[4](category)
就是这个套路的缩小版。
bucket 数怎么定
bucket[N] 的 N 一旦写入
spec,改它就是分区演进(见第五节),新老桶号不可比。规划时按「每个桶的目标文件大小」反推:希望单桶单次写约落到一个合理大小的文件(如几十到上百
MB),就用「预估每批行数 × 行宽 ÷ 目标文件大小」估
N。宁可一开始 N 略大,也别频繁演进。bucket 用 32 位
Murmur3(seed=0)哈希,分布均匀性对一般业务键足够,但严重倾斜的键(少数值占绝大多数行)即便分桶也会让对应桶偏大——这类列更适合先
truncate 或换分区键。
分区不是排序
分区把文件分到不同「桶/目录元组」,桶内文件不保证有序。要让桶内也利于裁剪与范围扫,需要写入排序(sort
order,规范《Sorting》)或 compaction 时 sort/z-order(第 17
篇)。分区解决「跳过整批文件」,排序解决「文件内/文件间的有序裁剪」,两者互补。第
8 篇 dump 的 default-sort-order-id: 0
表示本例未设排序。
多参数 transform(V3)
表规范 V3 引入多参数
transform(一个分区/排序字段可由多个源列产生),扩展了分区表达力。该特性属于
V3(已采纳),但能否使用取决于所用引擎/库版本对 V3
的支持程度;本系列遇到时按「规范已定义、引擎支持视版本而定」标注,不当作随处可用。本文实验为
format-version=2,分区字段都是单源列。
九、小结
Iceberg 把「分区」从「用户维护的物理列 + 目录约定」改成「表配置的 transform + 元数据派生」,由此得到两个 Hive 给不了的能力:
- 隐藏分区:查询只在源数据列上写谓词,Iceberg
用 transform 的包含投影把它转成分区谓词来裁文件(保序
transform 支持范围谓词,
bucket只支持等值)。分区列不再是第二份数据,消除了分区裂脑,也免了「忘写分区谓词就全表扫」。 - 分区演进:改分区方案只新增一个带新 id
的 spec,老 spec 与老文件原地保留;每个 manifest 标明自己的
partition_spec_id,planning 时按文件各自的 spec 转换谓词,于是新旧布局并存、跨 spec 正确裁剪,一行历史数据都不用重写。
这两点都建立在第 8 篇的元数据树之上:分区元组存在 manifest 里、与目录名解耦,spec 是「历史全集 + 当前指针」。下一篇进入行级删除与 Merge-on-Read,看 delete file 如何挂到 data file 上,以及 V2 delete file 与 V3 deletion vector 的差异。
上一篇:Iceberg 元数据树
返回 系列目录
附录、扩展阅读与工程注记
下面是设计与演进分区时常踩的点,尽量锚定规范条款或实跑观察。
bucket 为何要丢符号位
Murmur3 哈希结果是有符号 32
位整数,可能为负。& Integer.MAX_VALUE(& 0x7fffffff)先清掉符号位再
% N,保证桶号落在
[0, N)(规范《Bucket Transform
Details》)。直接对负数取模在不同语言会得到负余数,破坏桶号连续性。
为什么 bucket 范围谓词不裁
bucket[N]
把相邻值哈希到任意桶,不保序。category > 'book'
在 bucket 维度无法对应一段连续桶号,所以包含投影对范围谓词在
bucket
维度退化为「不裁」(仍可用其他分区字段裁)。只有等值谓词
category = 'book' 才能投影成
category_bucket = bucket4('book')。
day 分区值为何是整数
day 结果类型是
date,但规范要求读端也接受 int(自
1970-01-01 起的天数)。实跑 scan plan 里看到
20455/20456
就是这个整数表示(2026-01-02、2026-01-03)。这也是为什么按
ts 的范围谓词能投影成对该整数的范围比较。
void 不是删除分区
void 把分区字段的 transform 换成「恒
null」,使该字段在新写入中失去区分度——在 V1
里用它「逻辑丢弃」一个分区字段(因为 V1 不能安全删字段)。V2
起可显式增删 partition field,但 void
仍可用于「停用某分区维度但不改字段 id 布局」。
演进只影响新写入
加了新 spec
后,只有之后写入的文件走新布局;历史文件保持老
spec。想让老数据也按新布局重新组织,要显式
rewrite_data_files(第 17
篇),这会读老文件、按新 spec 重写、产生新
snapshot——代价等同一次批量重写,不是「免费」的。
同一 manifest 不混 spec
规范《Manifests》要求一个 manifest 只装单一 spec
的文件,因为 manifest 的 schema(分区元组结构)由其 spec
决定。实跑 inspect.manifests 里 spec 0 与 spec
1 分属不同 manifest,正是这条规则的体现。
partition field id 不可乱动
演进时已有 partition field 的 id 必须保持不变(实跑里
ts_day 始终是 1000),因为这些 id 被用作
manifest 分区元组的字段 id。改 id 会让老 manifest
的分区元组对不上 spec。新字段从
last-partition-id 递增分配(这里 1001)。
目录名仅为可读性
ts_day=2026-01-03/category_bucket=0/
这类路径是写文件时的人类可读约定,Iceberg planning
不读目录名,只读 manifest
里的分区元组。把文件挪到别的目录(路径仍被 manifest
记录)不影响裁剪——这与 Hive「目录名即分区真理」相反。
隐藏分区不消除小文件
隐藏分区改变的是「分区如何被声明与裁剪」,不改变物理文件数。过细的分区(高基数
identity、过大的 bucket N、hour
粒度)照样产生大量小文件,仍需
compaction。分区设计要同时权衡「裁剪收益」和「小文件成本」。
写倾斜数据的注意
bucket
假设键分布大致均匀。若某些值占绝大多数行(热点键),即使分桶,热点值所在的桶也会偏大。可考虑对热点列先
truncate、或在应用层加盐(salting)打散后再
bucket——但加盐会改变等值查询的写法,要权衡。
与时间旅行的关系
分区演进产生新 snapshot,读老 snapshot 时看到的是当时的 spec 与文件布局(第 16 篇)。所以「演进后再回看历史快照」是自洽的:每个 snapshot 引用的 manifest 各自带 spec id,按当时状态裁剪。
跨 spec 查询的统计口径
inspect.partitions 把不同 spec
的分区分别列出(实跑里 spec 0 的分区
category_bucket 为 None,spec 1
的有值)。做容量分析时要意识到「同一张表的分区可能来自不同
spec」,不能假设所有分区都有相同的字段集合。
identity 分区的陷阱
identity
对高基数列(如时间戳原值、UUID)会产出近乎一行一分区的灾难性布局——这正是很多
Hive
表「分区爆炸」的根因。需要分桶/截断/时间归一的列,不要用
identity 直接分区。
参考资料
- Apache Iceberg Table Spec, Partitioning(iceberg.apache.org/spec)
- Apache Iceberg Table Spec, Partition Transforms / Bucket Transform Details / Truncate Transform Details
- Apache Iceberg Table Spec, Partition Evolution
- Apache Iceberg Table Spec, Scan Planning / Manifests(每 manifest 单一 spec、按写入 spec 转换谓词)
- Apache Iceberg Table Spec, Goals(reads planned on data values, not partition values)
apache/iceberg1.x 源码:org.apache.iceberg.transforms.{Bucket, Truncate, Days, Hours, Months, Years}、org.apache.iceberg.PartitionSpec、UpdatePartitionSpec- 实验环境:pyiceberg 0.11.1、pyarrow 24.0.0、Python
3.14.5、Linux 6.6(WSL2/Arch)、Intel i9-12900K、32
GB;catalog=SQLite,warehouse=本地
FS,
format-version=2
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据湖与开放表格式】Lakehouse 全景:从 Hive 表到开放表格式
Hive 目录式分区表把『表』等同于『一组目录加 metastore 里的分区行』,于是没有原子提交、planning 要 LIST 目录、schema 与分区演进常要重写。本文用这三个硬伤切入,讲清 lakehouse 把表拆成『不可变数据文件 + 可变元数据指针 + catalog』三层后各自解决了什么,并给出全系列的分层地图。
【数据湖与开放表格式】表格式为什么存在
目录式分区表(Hive 表)在对象存储上有三处硬伤:并发写部分提交、list planning 太贵、缺快照隔离与原子提交。本文拆开放表格式补上的四件事——原子提交、快照隔离、文件级统计裁剪、schema 与分区演进,并抽象出三家共有的『元数据指针 + 不可变数据文件』骨架。
【数据湖与开放表格式】Iceberg 元数据树
拆解 Iceberg 的四层元数据:catalog 指针 → metadata.json → manifest list(snapshot)→ manifest file → data file。讲清 snapshot 与 manifest 里的分区数据和列级 stats(lower/upper bound、null/value count)如何让一次查询不 list 目录就收敛到文件集合,并给出表规范 V1/V2/V3 的版本边界。基于 pyiceberg 0.11.1 真实建表逐层 dump。
【数据湖与开放表格式】行级删除与 Merge-on-Read
Iceberg 在不可变文件上做行级删除的两条路线:copy-on-write(重写整文件)与 merge-on-read(写 delete 文件,读时合并)。讲清 position delete 与 equality delete 的语义、字段与作用域规则,写放大/读放大的取舍,V2 delete file 到 V3 deletion vector(Puffin 承载)的差异与迁移,以及读路径如何把 data file 与 delete 合并出可见行。基于 pyiceberg 0.11.1 实测 CoW 写放大并观察 MoR 回退。