- 数据湖
- 大数据
- 数据存储
【免费下载链接】iceberg
Apache Iceberg
Apache Iceberg 的视图规范(View Spec)定义了与表格式(Table Format)同等地位的统一视图元数据格式,让视图可以像表一样被共享、版本化与回滚。本文以仓库中的 format/view-spec.md 规范文档为主体,结合core/src/main/java/org/apache/iceberg/view/下的源码实现与 docs/docs/view-configuration.md 配置说明,完整讲解视图元数据 JSON 格式的每个字段、版本(version)、表示(representation)与版本日志(version-log)机制,并给出可复现的元数据文件示例。读完本文,你将能够读懂任意一份 Iceberg 视图元数据文件、理解视图提交与回滚的底层原理,并掌握视图相关属性的配置方法。
背景与动机:为什么视图也需要统一格式
大多数计算引擎(例如 Trino、Apache Spark)都支持视图(View)。视图是一种逻辑表,可以被后续查询引用;视图本身不包含任何数据,而是保存一段查询语句,每次被引用时都会重新执行。
问题在于:每个引擎都以私有格式将视图元数据存储在各自选择的元数据存储(metastore)中。因此,即使多个引擎共享同一个 metastore 和存储系统,从一个引擎创建的视图,也很难被另一个引擎直接读取或修改。这与 Iceberg 表格式致力于解决的"跨引擎表共享"问题如出一辙。
Iceberg 视图规范的目标就是:提供一种与表格式并列的通用视图元数据格式,使视图能够在不同引擎之间无缝共享。本文档即为该规范的权威定义。
总体设计:视图元数据的存储与检索
视图元数据的存储方式完全镜像 Iceberg 表元数据的存储与检索方式:
- 视图元数据维护在**元数据文件(metadata files)**中;
- 对视图状态的任何修改都会生成一个新的视图元数据文件,并通过**原子交换(atomic swap)**完全替换旧文件;
- 与 Iceberg 表一样,这个原子交换被委托给按名称管理表/视图的metastore;
- 视图元数据文件记录视图的 schema、自定义属性、当前与历史版本以及其他元数据。
每个元数据文件都是自足(self-sufficient)的:它包含最近若干版本的完整历史,因此可以用于将视图回滚到之前的版本。
元数据位置与读写并发
原子交换是实现视图原子变更的基础:
- **读者(Readers)**使用加载视图元数据时处于当前状态的版本,在刷新并获取新的元数据位置之前,不受其他变更的影响;
- **写者(Writers)**乐观地创建视图元数据文件,假定在提交之前当前元数据位置不会被改变;写者完成更新后,通过将视图的元数据文件指针从基础位置(base location)交换到新位置来完成提交。
从源码结构看,这一机制对应 ViewOperations.java 与 BaseViewOperations.java 中的元数据读写与提交逻辑,与 Iceberg 表的TableOperations设计保持一致。
术语
- Schema(模式):视图中字段的名称与类型。
- Version(版本):视图在某个时间点的状态。
视图元数据(View Metadata)
视图版本元数据文件包含以下字段:
| 要求 | 字段名 | 描述 |
|---|---|---|
| required | view-uuid | 标识视图的 UUID,在视图创建时生成。实现必须在刷新元数据后,若视图 UUID 与期望的 UUID 不匹配时抛出异常 |
| required | format-version | 视图格式的整数版本号,必须为1 |
| required | location | 视图的基础位置(base location),用于构造元数据文件位置 |
| required | schemas | 已知 schema 的列表 |
| required | current-version-id | 视图当前版本的 ID(version-id) |
| required | versions | 已知版本的列表 [1] |
| required | version-log | 版本日志条目列表,记录每次current-version-id变更的时间戳与version-id |
| optional | properties | 字符串到字符串的视图属性映射 [2] |
注:
- 需要保留的版本数量由视图属性
version.history.num-entries控制。- 属性用于
comment等元数据以及影响视图维护的设置,不应用于存储任意元数据。
源码中的字段定义
这些字段在源码中有直接对应实现。在 ViewMetadata.java 中,ViewMetadata接口定义了uuid()、formatVersion()、location()、schemas()、currentVersionId()、versions()、history()、properties()等访问方法,并声明了SUPPORTED_VIEW_FORMAT_VERSION = 1与DEFAULT_VIEW_FORMAT_VERSION = 1——这与规范中"格式版本必须为 1"的约束一致。同时check()方法会对formatVersion进行校验:必须满足0 < formatVersion <= SUPPORTED_VIEW_FORMAT_VERSION,否则抛出IllegalArgumentException。
JSON 的序列化/反序列化由 ViewMetadataParser.java 完成,其中明确定义了全部 JSON 键:view-uuid、format-version、location、current-version-id、versions、version-log、properties、schemas。注意:序列化时properties仅在非空时写出,schemas与versions、version-log均以数组形式写出。该解析器还支持读写 GZIP 压缩(默认gzip,详见后文属性部分)。
视图属性
properties字段可以承载两类内容:元数据(如comment)与影响视图维护行为的设置。仓库的 ViewProperties.java 集中定义了这些属性的常量与默认值,配合 docs/docs/view-configuration.md 可得到完整参数表:
| 属性 | 默认值 | 描述 |
|---|---|---|
write.metadata.compression-codec | gzip | 元数据压缩编解码器:none或gzip |
write.metadata.path | 视图位置 +/metadata | 视图元数据文件的基础位置;设置后元数据文件直接写入该路径下,不再追加/metadata |
version.history.num-entries | 10 | 控制要保留的versions数量 |
replace.drop-dialect.allowed | false | 控制在 replace 操作期间是否允许丢弃某种 SQL 方言 |
视图行为属性(提交重试相关):
| 属性 | 默认值 | 描述 |
|---|---|---|
commit.retry.num-retries | 4 | 提交失败前的重试次数 |
commit.retry.min-wait-ms | 100 | 重试提交前的最小等待时间(毫秒) |
commit.retry.max-wait-ms | 60000(1 分钟) | 重试提交前的最大等待时间(毫秒) |
commit.retry.total-timeout-ms | 1800000(30 分钟) | 一次提交的总重试超时时间(毫秒) |
这些属性可以在创建/替换视图(CREATE/REPLACE VIEW)时设置,也可以通过更新属性(updateProperties)的 API 设置,对应源码注释 "View properties that can be set during CREATE/REPLACE view or using updateProperties API"。
版本(Versions)
versions列表中的每个版本是一个结构体,包含以下字段:
| 要求 | 字段名 | 描述 |
|---|---|---|
| required | version-id | 版本的 ID |
| required | schema-id | 视图版本对应 schema 的 ID |
| required | timestamp-ms | 版本创建的时间戳(距 epoch 的毫秒数) |
| required | summary | 关于版本的摘要元数据字符串映射 |
| required | representations | 视图定义的表示列表 |
| optional | default-catalog | 当 SELECT 中的引用不包含 catalog 时使用的 catalog 名 |
| required | default-namespace | 当 SELECT 中的引用是单个标识符时使用的命名空间 |
当default-catalog为null或未设置时,必须使用存储该视图的 catalog 作为默认 catalog。
源码实现
ViewVersionParser.java 完整实现了版本的 JSON 读写,其字段常量与规范一一对应:version-id、timestamp-ms、schema-id、summary、representations、default-catalog、default-namespace。值得注意的实现细节:
default-catalog仅在非 null 时才写出(if (version.defaultCatalog() != null));default-namespace通过Namespace的多级 levels 数组写出——这就是示例 JSON 中"default-namespace": [ "default" ]采用数组形式的原因;summary使用字符串映射(JsonUtil.writeStringMap)。
另外,从 ViewMetadata.java 可以看到,ViewMetadata提供了按 ID 索引版本与 schema 的方法(versionsById、schemasById),并在访问当前版本/当前 schema 时校验其 ID 必须真实存在,否则抛出IllegalArgumentException。
摘要(Summary)
摘要(Summary)是视图版本的字符串到字符串元数据映射。规范文档化的常见元数据键如下:
| 要求 | 键 | 值 |
|---|---|---|
| optional | engine-name | 创建该视图版本的引擎名称 |
| optional | engine-version | 创建该视图版本的引擎版本 |
例如示例中"summary": { "engine-name": "Spark", "engine-version": "3.3.2" },表明该版本由 Spark 3.3.2 创建。
表示(Representations)
视图定义可以有多种表示方式。表示(Representation)是表达视图定义的规范化形式。
关键规则:
- 一个视图版本可以拥有多个表示,同一版本的所有表示必须表达相同的底层定义,引擎可以自由选择使用哪一种;
- 视图版本是不可变的(immutable)。版本一旦创建就不能修改,因此该版本的表示也不能更改。如果视图定义发生变化(或需要新增表示),必须创建新版本。
每个表示至少包含一个公共字段type,取值如下:
sql:定义视图的 SQL SELECT 语句
SQL 表示
SQL 表示以 SQL SELECT 语句存储视图定义,并携带 SQL 方言(dialect)等元数据。
一个视图版本可以包含不同方言的多个 SQL 表示,但每种方言最多一个 SQL 表示。
| 要求 | 字段名 | 类型 | 描述 |
|---|---|---|---|
| required | type | string | 必须为sql |
| required | sql | string | SQL SELECT 语句 |
| required | dialect | string | sqlSELECT 语句的方言(如"trino"或"spark") |
例如:
USE prod.defaultCREATE OR REPLACE VIEW event_agg ( event_count COMMENT 'Count of events', event_date) AS SELECT COUNT(1), CAST(event_ts AS DATE) FROM events GROUP BY 2上述创建语句会产生如下的sql表示元数据:
| 字段名 | 值 |
|---|---|
type | "sql" |
sql | "SELECT\n COUNT(1), CAST(event_ts AS DATE)\nFROM events\nGROUP BY 2" |
dialect | "spark" |
如果创建语句在AS之前没有包含列名或注释,则这些字段应被省略。示例中event_count(带Count of events注释)与event_date字段别名必须是视图版本schema的一部分。
源码实现
SQL 表示的实现集中在 BaseSQLViewRepresentation.java 与 SQLViewRepresentationParser.java:
SQLViewRepresentationParser定义了sql与dialect两个 JSON 键,序列化时依次写出type(由公共ViewRepresentationParser.TYPE提供)、sql、dialect三个字段;- 反序列化时要求节点必须是 JSON 对象,否则抛出
IllegalArgumentException; - 此外,仓库中还存在 UnknownViewRepresentation.java,用于处理未来可能出现的未知表示类型,体现了格式的前向兼容设计。
版本日志(Version log)
版本日志追踪视图当前版本的变更历史。它是视图的历史记录,可以用于重建在某个时间点视图所对应的版本。
需要注意:版本日志记录的不是版本的创建时间(创建时间存储在各版本自身的元数据中)。一个版本可以在版本日志中出现多次,表示视图定义被回滚过。
version-log中的每个条目是一个结构体:
| 要求 | 字段名 | 描述 |
|---|---|---|
| required | timestamp-ms | 视图current-version-id被更新的时间戳(距 epoch 的毫秒数) |
| required | version-id | current-version-id被设置为的 ID |
源码实现
ViewHistoryEntryParser.java 实现了版本日志条目的 JSON 读写,仅包含version-id与timestamp-ms两个字段,与规范完全一致。
在 ViewMetadata.java 的setCurrentVersionId中,可以看到版本日志的生成逻辑:每次切换当前版本时都会构造一个ViewHistoryEntry;如果该版本是在本次变更中新增的,则使用该版本自身的时间戳,否则使用当前系统时间(System.currentTimeMillis())——这正好处理了"视图定义在历史上曾被回滚、如今再次被激活"的场景。
附录 A:完整示例
以下通过一个完整示例说明 JSON 元数据文件格式。
假设发生如下操作序列:
USE prod.defaultCREATE OR REPLACE VIEW event_agg ( event_count COMMENT 'Count of events', event_date) COMMENT 'Daily event counts' AS SELECT COUNT(1), CAST(event_ts AS DATE) FROM events GROUP BY 2生成的元数据 JSON 文件如下。注意其路径有意与 Iceberg 表的路径相似,使用metadata目录:
s3://bucket/warehouse/default.db/event_agg/metadata/00001-(uuid).metadata.json{ "view-uuid": "fa6506c3-7681-40c8-86dc-e36561f83385", "format-version" : 1, "location" : "s3://bucket/warehouse/default.db/event_agg", "current-version-id" : 1, "properties" : { "comment" : "Daily event counts" }, "versions" : [ { "version-id" : 1, "timestamp-ms" : 1573518431292, "schema-id" : 1, "default-catalog" : "prod", "default-namespace" : [ "default" ], "summary" : { "engine-name" : "Spark", "engine-version" : "3.3.2" }, "representations" : [ { "type" : "sql", "sql" : "SELECT\n COUNT(1), CAST(event_ts AS DATE)\nFROM events\nGROUP BY 2", "dialect" : "spark" } ] } ], "schemas": [ { "schema-id": 1, "type" : "struct", "fields" : [ { "id" : 1, "name" : "event_count", "required" : false, "type" : "int", "doc" : "Count of events" }, { "id" : 2, "name" : "event_date", "required" : false, "type" : "date" } ] } ], "version-log" : [ { "timestamp-ms" : 1573518431292, "version-id" : 1 } ] }视图更新产生新元数据文件
每一次变更都会产生一个新的元数据 JSON 文件。在下面的示例中,底层 SQL 被修改为使用完全限定的表名:
USE prod.other_db; CREATE OR REPLACE VIEW default.event_agg ( event_count COMMENT 'Count of events', event_date) COMMENT 'Daily event counts' AS SELECT COUNT(1), CAST(event_ts AS DATE) FROM prod.default.events GROUP BY 2更新视图会产生一个完全替换旧文件的新元数据文件:
s3://bucket/warehouse/default.db/event_agg/metadata/00002-(uuid).metadata.json{ "view-uuid": "fa6506c3-7681-40c8-86dc-e36561f83385", "format-version" : 1, "location" : "s3://bucket/warehouse/default.db/event_agg", "current-version-id" : 2, "properties" : { "comment" : "Daily event counts" }, "versions" : [ { "version-id" : 1, "timestamp-ms" : 1573518431292, "schema-id" : 1, "default-catalog" : "prod", "default-namespace" : [ "default" ], "summary" : { "engine-name" : "Spark", "engine-version" : "3.3.2" }, "representations" : [ { "type" : "sql", "sql" : "SELECT\n COUNT(1), CAST(event_ts AS DATE)\nFROM events\nGROUP BY 2", "dialect" : "spark" } ] }, { "version-id" : 2, "timestamp-ms" : 1573518981593, "schema-id" : 1, "default-catalog" : "prod", "default-namespace" : [ "default" ], "summary" : { "engine-name" : "Spark", "engine-version" : "3.3.2" }, "representations" : [ { "type" : "sql", "sql" : "SELECT\n COUNT(1), CAST(event_ts AS DATE)\nFROM prod.default.events\nGROUP BY 2", "dialect" : "spark" } ] } ], "schemas": [ { "schema-id": 1, "type" : "struct", "fields" : [ { "id" : 1, "name" : "event_count", "required" : false, "type" : "int", "doc" : "Count of events" }, { "id" : 2, "name" : "event_date", "required" : false, "type" : "date" } ] } ], "version-log" : [ { "timestamp-ms" : 1573518431292, "version-id" : 1 }, { "timestamp-ms" : 1573518981593, "version-id" : 2 } ] }对比两份元数据文件,可以清晰看到规范设计的精髓:
view-uuid保持不变——它标识视图本身,与版本无关;current-version-id从 1 变为 2——当前指针指向最新版本;versions数组包含两个版本——元数据文件是自足的,完整保留了 v1 与 v2,因此可以把视图回滚到 v1;version-log追加了新条目——记录了current-version-id的每一次切换(含时间戳);schemas保持 v1 的 schema——本次修改未改变列定义,schema-id仍为 1,说明 schema 是按 ID 复用而非重复存储的。
测试验证与实现一致性
仓库的单元测试对规范行为进行了覆盖验证,例如 TestViewMetadata.java 中验证了version.history.num-entries必须为正数("version.history.num-entries must be positive but was 0"),与 ViewProperties.java 中该属性默认值为10的实现保持一致。
从源码结构还可以推断:ViewMetadata通过MetadataUpdate(如AddViewVersion、SetCurrentViewVersion、SetLocation、UpgradeFormatVersion)来追踪一次变更中累积的修改,再统一落盘为新元数据文件——这正是"所有变更原子地写入新文件并交换指针"这一设计在代码层面的具体实现。
总结
Apache Iceberg 视图规范为视图定义了与表格式并列的通用元数据格式,核心要点可概括为:
- 自足的历史文件:每个视图元数据文件包含完整版本历史,支持回滚;
- 原子交换:提交通过 metastore 原子地切换元数据指针,读者无感知;
- 不可变版本:任何定义变化都产生新版本,版本一经创建不可修改;
- 多表示支持:同一版本可同时携带多种方言的 SQL 表示,供不同引擎选择;
- 版本日志:记录
current-version-id的每次切换,可重建任意时间点的视图状态。
该规范已完整落地于 core/src/main/java/org/apache/iceberg/view/ 的 Java 实现中,配合 docs/docs/view-configuration.md 中的属性配置,Iceberg 视图真正实现了"一次创建、跨引擎共享、可版本回滚"的目标。
- 数据湖
- 大数据
- 数据存储
【免费下载链接】iceberg
Apache Iceberg
相关推荐
Apache Iceberg 元数据管理终极指南:深入解析表格式规范与最佳实践
Apache Iceberg 元数据管理终极指南:深入解析表格式规范与最佳实践 Apache Iceberg 是一款开源的大数据存储库,专为处理大量时间序列数据
数据湖大数据数据存储Backstage v1.12.0 版本解析:Catalog 游标分页、Scaffolder Zod 动作定义与后端系统导出重命名
Backstage v1.12.0 版本解析:Catalog 游标分页、Scaffolder Zod 动作定义与后端系统导出重命名 本篇技术指南围绕 Backs
数据湖大数据数据存储Egg.js 视图模板渲染(egg-view):统一视图引擎架构与实战指南
Egg.js 视图模板渲染(egg view):统一视图引擎架构与实战指南 在 Egg.js 应用中,绝大多数场景都需要“先取数据、再渲染模板”,因此必须引入对
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考