TT Lab
开始
学习 学习路径 课程

湖仓表格式 — 从元数据理解 Apache Iceberg

把按月分区的表改成按天 — 旧文件保持不动

在 TT Lab 中继续学习

目标

只对源列(order_ts)设置转换规则来划分,建立隐藏分区,并通过规划测出一个条件会打开多少个文件、多少行。在向表写入的中途,把按月改成按天(分区演进),并通过 metadata 确认:旧文件仍按旧规则原样保留,只有新文件才遵循新规则。

为什么重要

Hive 风格的表把分区做成列。要另外设置 order_date 这样的列,而且写入的人和读取的人都得知道这一列。读取一方如果只对 order_ts 设置条件,就一个分区也跳不过;写入一方如果时区算错了,行就会进入错误的分区。而且,想改变分区方式,就得把整张表重写一遍。 Iceberg 把分区做成规则。像 days(order_ts) 这样,只写源列和转换,引擎就会为每个文件计算分区值并写进清单,读取的一方仅凭 order_ts 条件就能跳过文件。规则带着 partition spec 的 ID 累积在 metadata 中,所以即使改变规则,旧文件仍按旧 spec 继续读取。觉得“好像得重写”,是最常见的误解。

步骤

  1. 用 /root/ice/part/month.py(应用 ice-part-month)以 PARTITIONED BY (months(order_ts)) 创建 lake.part.orders,并把三月整月(31 个文件)一次性提交。
  2. 用 /root/ice/part/plan1.py(pyiceberg)对 order_ts 为 2026-03-10 这一天的条件做规划,写入 /root/ice/part/out/plan_march.json。
  3. 用 /root/ice/part/evolve.py(应用 ice-part-evolve)把 months(order_ts) 改为 days(order_ts)。
  4. 用 /root/ice/part/april.py(应用 ice-part-april)一次性提交四月整月(30 个文件)。
  5. 用 /root/ice/part/plan2.py 对 3 月 10 日和 4 月 10 日的条件做规划,写入 /root/ice/part/out/plan.json。
  6. 用 /root/ice/part/bucket.py(应用 ice-part-bucket)以 PARTITIONED BY (bucket(4, customer_id)) 创建 lake.part.customers,并写入客户数据。
  7. 用 /root/ice/part/specs.py 把 lake.part.orders 的默认 spec ID 和各 spec 的数据文件数写入 /root/ice/part/out/specs.json。
  8. 在 /root/ice/part/report.md 中写出 ## 숨은 파티셔닝、## 파티션 진화、## 버킷 三个小节。

参考

按月划分的表——没有分区列

创建 /root/ice/part/month.py,应用名称为 ice-part-month,以 PARTITIONED BY (months(order_ts)) 创建 lake.part.orders(六个列,'format-version' = '2'),并用一次 append() 写入 /data/ice/orders/2026-03-*.csv 共 31 个文件。

months(order_ts) 使用自 1970 年 1 月起计数的月数作为分区值。请确认 schema 中不会出现新列。评分器会检查 spec 0 的转换是不是 month,第一次提交是不是整个三月,以及这些文件的分区值是否全部是 2026 年 3 月。

一天的条件会打开的文件——按月的局限

用 /root/ice/part/plan1.py(pyiceberg),为 order_ts >= 2026-03-10T00:00:00+00:00 且 < 2026-03-11T00:00:00+00:00 的条件创建扫描,把规划的文件数、这些文件的 record_count 之和、实际命中的行数,按 {"files_planned", "records_scanned", "rows"} 的格式写入 /root/ice/part/out/plan_march.json(现在表里只有三月)。

条件只针对 order_ts 设置,却也会与分区值(月)比较,从而排除其他月份的文件。但即使只想要 3 月 10 日这一天,三月的文件也必须把整个月的数据整体读取。records_scanned 与 rows 的差距就是这部分成本。评分器会以第一个快照(只有三月的时候)为准重新做同样的规划来对照。

分区演进——只有 metadata 会变

创建 /root/ice/part/evolve.py,应用名称为 ice-part-evolve,并运行 ALTER TABLE lake.part.orders REPLACE PARTITION FIELD months(order_ts) WITH days(order_ts)。

会新增 spec 1,默认值(default-spec-id)变为 1。不会产生快照,数据文件也保持不变——只有之后写入的文件才遵循新规则。评分器会检查 spec 列表、默认 spec 和快照数。

四月提交——只有新文件使用新 spec

创建 /root/ice/part/april.py,应用名称为 ice-part-april,用一次 append() 写入 /data/ice/orders/2026-04-*.csv 共 30 个文件。不要重写三月的文件。

新提交的文件是 spec_id 1,每一天分别记录。三月的文件仍是 spec_id 0(月),与它们共存于同一个快照中。读取的引擎会针对每个 spec 分别做规划,所以即使两种规则混在一起,结果也一样。评分器还会检查三月的文件是否仍是第一次提交的路径和 spec。

同样是一天的条件,成本不同

用 /root/ice/part/plan2.py 分别对 3 月 10 日这一天和 4 月 10 日这一天的条件做规划,按 {"march": {"files_planned", "records_scanned", "rows"}, "april": {…}} 的格式写入 /root/ice/part/out/plan.json。

两天的行数差不多,但需要读取的行数相差很大。三月要把整个月的文件整体打开,四月只打开那一天的一个文件。评分器会用现在的表做同样的规划来对照,并检查四月这一边是否只读取了当天的行。

基数高的列用 bucket

创建 /root/ice/part/bucket.py,应用名称为 ice-part-bucket,以 PARTITIONED BY (bucket(4, customer_id)) 创建 lake.part.customers(customer_id STRING, tier STRING, region STRING, signup_date DATE),并写入 /data/ice/customers.csv。

bucket(N, 列) 把值的 32 位 murmur3 哈希对 N 取余,作为分区值。这是规范规定的哈希,所以不管哪个引擎来计算,得到的都是同一个桶——评分器会用 pyiceberg 为每位客户重新计算桶,并与文件的分区值对照。

一张表中的两种规则

用 /root/ice/part/specs.py(pyiceberg),把 lake.part.orders 的默认 spec ID 和各 spec 的数据文件数,按 {"default_spec_id": 정수, "files_by_spec": {"0": 정수, "1": 정수}} 的格式写入 /root/ice/part/out/specs.json(占位符均为整数)。

用 tbl.inspect.files() 的 spec_id 列来统计(content 为 0 的是数据文件)。一个快照中混有两个 spec 的文件,是正常的。

把分区设计变成团队规则

在 /root/ice/part/report.md 中写出 ## 숨은 파티셔닝、## 파티션 진화、## 버킷 三个小节。第二节以数字写入第 5 步中三月和四月的两个 records_scanned 值。

一开始选择按月划分是错了吗?还是数据增多之后不再适合了?另外请写出,如果有朝一日必须重写旧的三月文件,理由应该是什么。