Iceberg Schema 基础:从定义到版本号

好,咱们正式开始聊 Schema。很多同学一听到「Schema 治理」就觉得是高大上的理论,其实说白了,Schema 就是一张表的结构说明书——它告诉系统这张表有哪些列、每列存什么类型、数据怎么组织。Iceberg 的 Schema 设计,是我见过最灵活也最严谨的。今天我就带你把它拆开看看。

Schema 定义:一张表的结构说明书

在 Iceberg 里,Schema 的定义非常明确。它不是一个可有可无的元数据,而是表的核心契约。每次你往表里写数据,Iceberg 都会拿 Schema 来校验——字段名对不对、类型匹不匹配、嵌套结构是否合规。

我个人习惯把 Schema 理解成「表的数据蓝图」。你想想看,没有蓝图就盖房子,那不得塌?Iceberg 的 Schema 定义包含三个核心要素:

  • 字段列表:每个字段有唯一的 ID、名称、类型
  • 字段 ID:这是 Iceberg 的杀手锏,字段改名不影响数据读取
  • Schema 版本号:每次变更都会生成新版本,历史版本可追溯

关键点:Iceberg 的 Schema 是不可变的。每次修改都会生成一个新的 Schema 版本,旧版本不会被覆盖。这意味着你可以随时回溯到任意历史 Schema。

我在项目中遇到过一个问题:同事直接改了 Hive 表的列名,结果下游 ETL 全部报错。换成 Iceberg 后,字段 ID 机制让改名变得安全——底层数据文件只认 ID,不认名字。嗯,这就是 Schema 设计的魅力。

数据类型体系:Iceberg 的「类型宇宙」

Iceberg 的数据类型体系,比 Hive 和 Parquet 都要丰富。它把类型分成了几大类:

类别 类型 说明
基础类型 boolean, int, long, float, double, decimal(P,S) 跟标准 SQL 类型基本一致
时间类型 date, time, timestamp, timestamptz 支持时区,精度到微秒
字符串类型 string, uuid, fixed(L), binary string 是 UTF-8,uuid 是 16 字节
复杂类型 struct, list, map 支持嵌套,这是 Iceberg 的强项

避坑指南:我曾经在项目中用 decimal(38,0) 存金额,结果发现精度浪费严重。Iceberg 的 decimal 支持任意精度,但建议根据业务实际需求来定,别盲目用最大精度。

这里有个细节要注意:Iceberg 的 timestamp 和 timestamptz 是两种不同的类型。前者不带时区,后者带时区。我建议跨时区业务统一用 timestamptz,否则数据迁移时容易出问题。

字段与嵌套结构:Struct、List、Map 的玩法

Iceberg 的嵌套结构,说白了就是支持「字段里面套字段」。这在处理 JSON 数据、事件日志、复杂对象时特别有用。

举个例子,假设我们要存用户订单信息:

-- 用 SQL 定义嵌套 Schema
CREATE TABLE orders (
  order_id BIGINT,
  user_info STRUCT<
    name: STRING,
    address: STRUCT<
      city: STRING,
      street: STRING,
      zip: STRING
    >
  >,
  items LIST<STRUCT<
    product_id: BIGINT,
    quantity: INT,
    price: DECIMAL(10,2)
  >>,
  tags MAP<STRING, STRING>
)
USING iceberg;

你看,user_info 是一个 Struct,里面又嵌套了一个 address Struct。items 是一个 List,每个元素又是一个 Struct。tags 是 Map,键值对都是字符串。

嵌套结构的好处是什么?我总结三点:

  • 减少 JOIN:相关数据可以存在一起,查询时一次 IO 搞定
  • 保持数据完整性:嵌套结构天然表达了数据之间的从属关系
  • 灵活扩展:可以在 Struct 里加字段,不影响已有数据

注意:嵌套太深会影响查询性能。我个人建议嵌套层级不要超过 3 层。超过这个深度,考虑用 Flatten 或者拆表。

我记得有一次,业务方要求在一个字段里存 10 层嵌套的 JSON。我直接拒绝了——不是 Iceberg 不支持,而是查询起来太痛苦。你想想看,每次查数据都要 a.b.c.d.e.f,写代码的人不疯才怪。

Schema 版本号:Iceberg 的「时光机」

Schema 版本号是 Iceberg 最让我佩服的设计之一。每次 Schema 变更,Iceberg 都会:

  1. 生成一个新的 Schema 对象
  2. 分配一个递增的版本号(从 0 开始)
  3. 记录变更时间、变更内容
  4. 把新 Schema 写入表的元数据文件

这意味着什么?你可以随时查看表的历史 Schema:

-- 查看当前 Schema
DESCRIBE TABLE orders;

-- 查看 Schema 历史(Spark SQL)
DESCRIBE HISTORY orders;

-- 按版本号读取历史数据
SELECT * FROM orders VERSION AS OF 1;

版本号机制带来的好处很明显:

  • 回溯能力:数据出问题时,可以按旧 Schema 读取历史数据
  • 兼容性保障:新 Schema 必须兼容旧 Schema,否则 Iceberg 会拒绝变更
  • 审计追踪:谁在什么时候改了 Schema,一目了然

核心原则:Iceberg 的 Schema 版本号是单调递增的,永远不会回退。即使你删除了某个字段,版本号也不会复用。这保证了 Schema 历史的完整性和可追溯性。

我曾经用这个特性救过一次火。某天凌晨,数据管道突然报错,原因是上游改了字段类型。我直接查了 Schema 历史,发现昨天有人把 price 从 DECIMAL(10,2) 改成了 STRING。嗯,回滚到上一个版本,数据就正常了。要是没有版本号,这种问题排查起来至少要多花半天。

本章知识体系总览

下面这张图,把 Iceberg Schema 的核心要素串起来了。你可以把它当作本章的「思维导图」:

Iceberg Schema 核心要素结构图 Iceberg Schema Schema 定义 数据类型体系 嵌套结构 Schema 版本号 字段 ID + 名称 不可变契约 基础 + 时间 + 字符串 复杂类型 Struct / List / Map 嵌套 ≤ 3 层 单调递增 历史可回溯 Schema 是 Iceberg 表治理的基石

这张图把本章的核心内容串起来了。你看,Schema 定义是基础,数据类型体系是骨架,嵌套结构是扩展能力,版本号是时间轴。四者缺一不可。

好了,Schema 基础就聊到这里。记住一句话:Schema 是 Iceberg 表治理的基石。后面的章节,我们会在这个基础上,一步步搭建完整的 Schema 治理体系。