本文档总结了 Apache Iceberg 的核心特性、最佳实践和高级用法,帮助数据工程师充分利用 Iceberg 提供的功能。

目录

表属性与配置

核心表属性

创建 Iceberg 表时的关键属性配置:

CREATE TABLE my_table (...)
USING iceberg
TBLPROPERTIES (
  -- 格式与版本
  'format' = 'iceberg/parquet',
  'format-version' = '2',
  
  -- 事务与更新
  'write.upsert.enabled' = 'true',
  'write.merge.mode' = 'merge-on-read',
  'identifier-fields' = '[user_id, event_date]',
  
  -- 历史保留
  'history.expire.max-snapshot-age-ms' = '259200000',  -- 3天
  'history.expire.max-ref-age-ms' = '259200000'
)

分区优化

-- 按日期分区的示例
CREATE TABLE events (
  event_time timestamp,
  user_id bigint,
  event_type string,
  data map<string, string>
)
USING iceberg
PARTITIONED BY (days(event_time))

标识字段注意事项

使用 identifier-fields 时需要注意:

  • 标识字段必须是非空字段 (NOT NULL)
  • 标识字段应该是唯一的
  • 标识字段应该是原子类型(不能是复杂类型如 map, array, struct)

元数据表

Iceberg 提供多种元数据表以支持高级查询和管理操作:

元数据表之间的关系

Iceberg 的元数据表形成一个分层结构,从高层概览到底层细节:

顶层(表历史) → 中层(快照和清单) → 底层(数据文件)

层次关系图

history ──┐
          ├→ snapshots ──┐
refs ─────┘              ├→ manifests ─→ files
metadata_log_entries ────┘              │
                       partitions ──────┘

元数据表对比

表名 数据范围 时间维度 主要用途 关键字段
history 表级别 全部历史 表操作审计 made_current_at, snapshot_id
snapshots 表级别 全部历史 快照管理 snapshot_id, parent_id, operation
files 文件级别 当前状态 数据文件分析 file_path, record_count, column_sizes
all_data_files 文件级别 全部历史 历史文件分析 snapshot_id, file_path, record_count
manifests 清单级别 当前状态 清单文件管理 path, added_files_count
all_manifests 清单级别 全部历史 历史清单分析 snapshot_id, path
partitions 分区级别 当前状态 分区统计 partition, record_count, file_count
refs 引用级别 当前状态 分支和标签管理 name, snapshot_id, type
metadata_log_entries 元数据级别 全部历史 元数据文件跟踪 timestamp, file, latest_snapshot_id
entries 记录级别 内部状态 底层调试 status, snapshot_id, data_file

实用组合查询示例

不同元数据表可以结合使用,提供强大的分析能力:

-- 找出特定快照中新增的大文件
SELECT f.file_path, f.file_size_in_bytes, f.record_count
FROM my_table.files f
JOIN my_table.manifests m ON f.manifest_path = m.path
WHERE m.added_snapshot_id = 1234567890
AND f.file_size_in_bytes > 100000000
ORDER BY f.file_size_in_bytes DESC;

-- 跟踪特定分区的变更历史
SELECT s.committed_at, s.operation, p.record_count
FROM my_table.snapshots s
JOIN my_table.partitions p
  ON p.partition = 'date=2025-05-25'
WHERE s.snapshot_id >= 
  (SELECT min(snapshot_id) FROM my_table.manifests 
   WHERE path IN (SELECT manifest_path FROM my_table.files 
                  WHERE partition = 'date=2025-05-25'))
ORDER BY s.committed_at;

1. history 元数据表

跟踪表的所有历史操作和变更记录:

-- 查看表的历史变更
SELECT * FROM my_table.history ORDER BY made_current_at DESC;

主要字段:

  • made_current_at: 操作时间
  • snapshot_id: 操作产生的快照ID
  • parent_id: 父快照ID
  • is_current_ancestor: 是否是当前状态的祖先

2. snapshots 元数据表

提供表的所有快照详情:

-- 查看表的快照
SELECT snapshot_id, operation, committed_at, summary 
FROM my_table.snapshots 
ORDER BY committed_at DESC;

主要字段:

  • snapshot_id: 快照唯一标识
  • parent_id: 父快照ID
  • operation: 操作类型(append/delete/overwrite/merge)
  • manifest_list: 快照的清单列表文件
  • committed_at: 提交时间
  • summary: 操作摘要(包含统计信息)

3. files 元数据表

列出表当前版本的所有数据文件:

-- 查看表的数据文件
SELECT file_path, record_count, file_size_in_bytes 
FROM my_table.files 
ORDER BY file_size_in_bytes DESC;

主要字段:

  • content: 文件内容类型(0=数据,1=删除)
  • file_path: 文件路径
  • file_format: 文件格式(通常是Parquet)
  • record_count: 记录数量
  • file_size_in_bytes: 文件大小
  • column_sizes: 各列的大小统计
  • value_counts: 各列的值计数
  • null_value_counts: 各列的NULL值计数
  • lower_bounds: 各列的最小值
  • upper_bounds: 各列的最大值

4. manifests 元数据表

列出表的清单文件:

-- 查看表的清单文件
SELECT path, added_snapshot_id, added_data_files_count 
FROM my_table.manifests;

主要字段:

  • path: 清单文件路径
  • length: 清单文件大小
  • partition_spec_id: 分区规范ID
  • added_snapshot_id: 添加此清单的快照ID
  • added_data_files_count: 添加的数据文件数
  • existing_data_files_count: 现有数据文件数
  • deleted_data_files_count: 删除的数据文件数

5. partitions 元数据表

提供表分区的统计信息:

-- 查看表的分区信息
SELECT partition, record_count, file_count 
FROM my_table.partitions 
ORDER BY record_count DESC;

主要字段:

  • partition: 分区值
  • record_count: 分区中的记录数
  • file_count: 分区中的文件数
  • spec_id: 分区规范ID

6. refs 元数据表

列出表的所有引用:

-- 查看表的引用
SELECT * FROM my_table.refs;

主要字段:

  • name: 引用名称
  • type: 引用类型
  • snapshot_id: 引用的快照ID
  • max_reference_age_in_ms: 引用最大保留时间
  • min_snapshots_to_keep: 最少保留快照数
  • max_snapshot_age_in_ms: 快照最大保留时间

使用refs表可以:

  • 查找可能丢失快照的引用
  • 查找特定引用的最新快照
  • 了解哪些分支的规则可能导致快照失效
-- 查找仅包含最大快照规则的引用
SELECT name, min_snapshots_to_keep, max_snapshot_age_in_ms
FROM my_table.refs
WHERE min_snapshots_to_keep IS NOT NULL AND max_snapshot_age_in_ms IS NOT NULL;

分支与快照管理

快照管理操作

Iceberg 提供多种操作来管理表快照:

1. set_current_snapshot

直接将表的当前状态指向特定快照:

-- 将表回滚到特定快照
CALL catalog.system.set_current_snapshot('my_table', 1234567890);

特点:

  • 直接操作,立即改变表状态
  • 无分支概念,直接修改主引用(main)
  • 创建线性历史记录
  • 适用于简单回滚和紧急修复

2. fast_forward

将分支更新到其后代快照:

-- 将main分支更新到feature分支的最新状态
CALL catalog.system.fast_forward('my_table', 'main', 'feature-branch');

特点:

  • 只能在源快照是目标分支直接后代时使用
  • 不创建新快照,只移动分支指针
  • 保留分支关系信息
  • 适用于线性开发流程

3. cherrypick_snapshot

选择性地将特定快照的变更应用到分支:

-- 将特定快照的变更应用到main分支
CALL catalog.system.cherrypick_snapshot(
  'my_table', 
  'main', 
  snapshot_id,
  map('cherry-pick.conflict-resolution-mode', 'PRESERVE_MAIN')
);

特点:

  • 创建新快照,包含选择的变更
  • 有冲突检测和解决机制
  • 保留完整历史并添加新节点
  • 适用于选择性功能集成

冲突解决模式:

  • PRESERVE_MAIN (默认):冲突时保留目标分支数据
  • PRESERVE_SOURCE:冲突时使用源快照数据
  • ERROR:冲突时抛出错误
  • MERGE:尝试合并冲突数据

分支管理

创建和管理分支:

-- 创建新分支
CALL catalog.system.create_branch('my_table', 'feature-x', snapshot_id);

-- 在特定分支上操作
INSERT INTO my_table FOR BRANCH 'feature-x' VALUES (...);

Write Ahead Protocol (WAP)

WAP 基础

WAP是一种两阶段提交协议,确保写入操作的原子性和隔离性:

-- 启用WAP
ALTER TABLE my_table SET TBLPROPERTIES (
  'write.wap.enabled' = 'true'
);

-- 使用WAP (Spark示例)
spark.conf.set("spark.wap.id", "wap-job-123");
spark.sql("INSERT INTO my_table SELECT * FROM source_table");

WAP 配置优化

-- WAP高级配置
ALTER TABLE my_table SET TBLPROPERTIES (
  'write.wap.enabled' = 'true',
  'write.object-storage.enabled' = 'true',  -- 对象存储优化
  'write.metadata.delete-after-commit.enabled' = 'true',  -- 延迟删除
  'write.folder-storage.path-style' = 'wap-friendly'  -- 路径优化
);

优化效果:

  • 启用对象存储优化:避免数据复制,只更新元数据引用
  • 启用延迟删除:将删除操作从提交关键路径移除
  • 设置WAP友好路径:优化存储布局,加速提交

WAP 自动恢复

配置WAP事务自动恢复:

-- Spark SQL中的自动恢复配置
SET spark.sql.iceberg.handle-wap-conflicts = 'auto';
SET spark.sql.iceberg.wap.auto-resume-enabled = 'true';
SET spark.sql.iceberg.wap.conflict-resolution-mode = 'preserve-incoming';

使用一致的WAP ID便于恢复:

-- 使用有意义的WAP ID
SET spark.wap.id = 'daily-etl-2025-05-27';

WAP ID不需要预先创建,设置后会在首次写入时自动初始化。

WAP 恢复场景

WAP恢复机制在以下情况最有价值:

  1. 基础设施故障

    • 节点崩溃
    • 网络中断
    • 存储系统暂时不可用
  2. 资源限制

    • 内存不足
    • 超出执行器限制
    • 存储配额超限
  3. 超时和中断

    • 长时间运行作业超时
    • 集群维护导致中断
  4. 用户控制的暂停/恢复

    • 手动暂停以优先处理更高优先级作业
    • 计划维护

优化技术

回顾窗口优化

回顾窗口(Planning Lookback Window)提供了更精细的文件级别过滤:

-- 启用回顾窗口优化
ALTER TABLE my_table SET TBLPROPERTIES (
  'read.split.planning-lookback.enabled' = 'true',
  'read.split.planning-lookback.window' = '2592000000'  -- 30天(毫秒)
);

优势:

  • 分析快照变更历史,精确识别文件变更
  • 高效支持增量处理
  • 减少I/O,提高查询性能
  • 尤其适合时间窗口分析和变更数据捕获

与标准Manifest过滤的区别:

  • 标准过滤:基于当前快照,静态元数据
  • 回顾窗口:考虑时间维度,分析变更历史

回顾窗口优化原理

回顾窗口优化通过构建和维护文件变更的时间索引来提高查询性能:

  1. 缓存构建与维护

    • 缓存是按需构建的,通常在首次查询触发时初始化
    • 每次提交新快照后增量更新缓存
    • 系统会跟踪在回顾窗口期限内的所有文件添加和删除事件
  2. 工作原理

    // 构建文件变更图
    fileChangeMap = HashMap<FileId, List<ChangeEvent>>();
    
    // 遍历快照记录文件变更
    foreach (snapshot in relevantSnapshots) {
      addedFiles = getAddedFiles(snapshot);
      deletedFiles = getDeletedFiles(snapshot);
    
      // 记录文件变更事件
      foreach (file in addedFiles/deletedFiles) {
        fileChangeMap.get(file.id).add(new Event(...));
      }
    }
    
  3. 应用场景比较

    • 高效场景:数据变更集中在表的小部分,查询覆盖长时间范围
      • 例如:一个1TB表,每天只修改1%的数据,查询30天变更只需扫描约9GB
    • 低效场景:数据频繁变化,或查询主要针对最新数据
      • 例如:如果50%的数据每天变化,优化收益有限且缓存开销大
  4. 与分区修剪的交互

    • 工作层次不同:分区修剪工作在物理布局层,回顾窗口工作在文件元数据层
    • 互补增强型:分区修剪减少分区范围,回顾窗口进一步在分区内过滤文件
    • 重叠冗余型:某些场景下两种优化可能有重叠效果

回顾窗口优化的权衡

启用回顾窗口优化有一定成本:

  1. 内存消耗

    • 需要在内存中维护文件变更历史缓存
    • 对于大型表或长时间窗口,可能消耗显著内存
  2. 查询规划时间

    • 增加查询规划阶段的时间
    • 对于简单查询,规划开销可能超过执行收益
  3. 调优建议

    -- 适中的配置示例
    ALTER TABLE my_table SET TBLPROPERTIES (
      'read.split.planning-lookback.enabled' = 'true',
      'read.split.planning-lookback.window' = '604800000',  -- 7天而不是30天
      'write.metadata.previous-versions-max' = '100'  -- 确保有足够的元数据历史
    );
    

清单文件优化

管理清单文件大小:

-- 设置清单文件优化参数
ALTER TABLE my_table SET TBLPROPERTIES (
  'commit.manifest.min-count-to-merge' = '100',  -- 低于此值的清单将被合并
  'commit.manifest.target-size-bytes' = '8388608',  -- 目标大小8MB
  'commit.manifest.max-count' = '5000'  -- 单个清单最多5000个条目
);

-- 主动重写清单文件
CALL catalog.system.rewrite_manifests(
  table => 'my_table',
  strategy => 'binpack'
);

清单过大导致的问题:

  • 顺序扫描开销增加
  • 内存使用增加
  • 序列化/反序列化成本提高
  • 并行处理受限

文件压缩与小文件合并

-- 配置文件大小
ALTER TABLE my_table SET TBLPROPERTIES (
  'write.target-file-size-bytes' = '536870912',  -- 512MB
  'write.distribution-mode' = 'hash'
);

-- 运行数据文件压缩
CALL catalog.system.rewrite_data_files(
  table => 'my_table',
  strategy => 'binpack',
  options => map('min-input-files','5', 'target-file-size-bytes', '536870912')
);

数据生命周期管理

Iceberg 提供了完整的数据生命周期管理机制,包括快照过期、垃圾收集和孤儿文件清理。这三种机制协同工作,维护表的性能和存储效率。

数据生命周期参数类别

Iceberg 的数据生命周期管理分为四个主要类别:

  1. 快照过期参数 - 控制哪些快照(表历史版本)应该被保留
  2. 垃圾收集参数 - 控制哪些物理文件可以被安全删除
  3. 孤儿文件清理参数 - 处理未被任何元数据引用的文件
  4. 元数据文件管理参数 - 控制元数据文件的保留和清理

快照过期参数

参数名 描述 默认值
history.expire.min-snapshots-to-keep 必须保留的最小快照数量 1
history.expire.max-snapshot-age-ms 快照的最大保留时间(毫秒) Long.MAX_VALUE

引用过期参数

参数名 描述 默认值
history.expire.max-ref-age-ms 已删除分支/标签的最大保留时间 Long.MAX_VALUE
history.expire.branches-to-keep 要保留的分支名称列表 (空)
history.expire.tags-to-keep 要保留的标签名称列表 (空)

垃圾收集参数

参数名 描述 默认值
gc.enabled 是否启用垃圾收集 true
gc.min-snapshots-to-keep 垃圾收集时保留的最小快照数 1
gc.max-snapshot-age-ms 垃圾收集时要保留的快照的最大年龄 Long.MAX_VALUE

孤儿文件清理参数

参数名 描述 默认值
write.orphan-files.purge.enabled 是否在提交时自动清理孤儿文件 false
write.orphan-files.retention-duration 常规孤儿文件保留时间(毫秒) 259200000 (3天)
write.wap.orphan-files.retention-ms WAP临时文件保留时间(毫秒) 86400000 (1天)

元数据文件参数

参数名 描述 默认值
write.metadata.delete-after-commit.enabled 启用元数据文件的延迟删除 false
write.metadata.previous-versions-max 保留的先前元数据版本数量 100

数据生命周期的级联关系

Iceberg 的数据清理遵循一个重要的级联关系:

快照过期 → 垃圾收集 → 物理文件删除
  1. 快照过期:标记快照为"已过期"(元数据操作)
  2. 垃圾收集:识别并删除仅被已过期快照引用的文件
  3. 孤儿文件清理:删除完全没有被元数据引用的文件

垃圾收集机制详解

垃圾收集是 Iceberg 数据清理的核心机制:

  1. 安全保证

    • 垃圾收集只会删除不再被任何活跃快照引用的文件
    • 如果文件被任何未过期的快照引用,它绝对不会被删除
    • gc.min-snapshots-to-keepgc.max-snapshot-age-ms 提供额外的安全层
  2. 工作原理

    • 系统识别所有"活跃快照"(未过期的快照)
    • 确定每个文件被哪些快照引用
    • 只删除那些完全不被任何活跃快照引用的文件
  3. 配置最佳实践

    • 可以安全地设置 gc.min-snapshots-to-keep 小于 history.expire.min-snapshots-to-keep
    • 这样不会导致文件被错误删除,因为活跃快照引用的文件总是受保护的
    • 通常建议将 gc.min-snapshots-to-keep 设置为 1-5,提供一个额外的安全网
  4. 垃圾收集的执行

    • 作为快照过期操作的一部分自动执行
    • 也可以通过 remove_orphan_files 存储过程单独执行

孤儿文件管理

孤儿文件是存在于表目录但未被任何元数据引用的文件:

  1. 产生原因

    • 快照过期但文件未被垃圾收集(例如 gc.enabled=false
    • 失败的写操作
    • WAP事务中断
    • 并发写入冲突
    • 手动误操作
  2. 两种类型的孤儿文件

    • 常规孤儿文件:主数据目录中未被引用的文件
    • WAP临时文件:WAP目录中的临时文件
  3. 清理配置建议

    • 孤儿文件的保留时间应大于或等于快照过期和垃圾收集的保护时间
    • WAP临时文件的保留时间应考虑WAP事务的最长运行时间

快照过期管理

配置和执行快照过期:

-- 表级过期配置(安全网)
ALTER TABLE my_table SET TBLPROPERTIES (
  'history.expire.min-snapshots-to-keep' = '20',  -- 保留至少20个快照
  'history.expire.max-snapshot-age-ms' = '2592000000'  -- 30天
);

-- 主动运行维护作业(更积极清理)
CALL catalog.system.expire_snapshots(
  table => 'my_table',
  older_than => CURRENT_TIMESTAMP() - INTERVAL 7 DAY,  -- 保留7天
  retain_last => 10  -- 至少保留10个快照
);

注意snapshots 元数据表会显示已过期的快照,只要包含这些快照的元数据文件仍然存在。

恢复已过期快照

已过期的快照可能可以恢复,前提是:

  1. 包含快照信息的元数据文件仍然存在
  2. 快照引用的数据文件尚未被垃圾收集删除

恢复方法:

-- 查找过期快照的ID
SELECT snapshot_id, committed_at 
FROM my_table.snapshots
WHERE committed_at > TIMESTAMP '2025-05-01 00:00:00'
ORDER BY committed_at;

-- 使用特定快照创建新分支
CALL catalog.system.create_branch(
  table => 'my_table',
  branch => 'recovered_snapshot',
  snapshot_id => 123456789
);

不同场景的生命周期配置

标准生产环境

ALTER TABLE standard_table SET TBLPROPERTIES (
  -- 快照过期配置
  'history.expire.min-snapshots-to-keep' = '20',
  'history.expire.max-snapshot-age-ms' = '1209600000',  -- 14天
  
  -- 垃圾收集配置
  'gc.enabled' = 'true',
  'gc.min-snapshots-to-keep' = '5',  -- 额外保护5个快照
  
  -- 元数据优化
  'write.metadata.delete-after-commit.enabled' = 'true',
  
  -- 孤儿文件清理配置
  'write.orphan-files.purge.enabled' = 'true',
  'write.orphan-files.retention-duration' = '1209600000'  -- 14天
);

高频更新环境

ALTER TABLE high_frequency_table SET TBLPROPERTIES (
  -- 快照过期较短
  'history.expire.min-snapshots-to-keep' = '10',
  'history.expire.max-snapshot-age-ms' = '259200000',  -- 3天
  
  -- 积极的垃圾收集
  'gc.enabled' = 'true',
  'gc.min-snapshots-to-keep' = '3',
  
  -- 更频繁的孤儿文件清理
  'write.orphan-files.purge.enabled' = 'true',
  'write.orphan-files.retention-duration' = '259200000'  -- 3天
);

元数据清理优化

优化元数据删除操作:

-- 启用延迟元数据删除
ALTER TABLE my_table SET TBLPROPERTIES (
  'write.metadata.delete-after-commit.enabled' = 'true',
  'write.metadata.previous-versions-max' = '50'  -- 保留50个版本
);

优势:

  • 减少锁持有时间
  • 提高提交可靠性
  • 支持元数据恢复

维护作业调度策略

建议的维护作业频率:

表类型 快照过期频率 垃圾收集频率 孤儿文件清理频率
高频更新表 每天运行1-2次 每天运行1次 每天运行1次
中频更新表 每周运行2-3次 每周运行1-2次 每周运行1次
低频更新表 每两周运行一次 每月运行一次 每月运行一次

表结构演进

Iceberg支持安全的Schema演进:

-- 添加新列
ALTER TABLE my_table ADD COLUMN user_email string AFTER user_id;

-- 重命名列
ALTER TABLE my_table RENAME COLUMN user_id TO customer_id;

名称映射支持:

-- 设置名称映射
ALTER TABLE my_table SET TBLPROPERTIES (
  'schema.name-mapping.default' = '{
    "mappings": [
      {"field-id": 1, "names": ["item_id", "item_identifier"]},
      {"field-id": 2, "names": ["transaction_id", "txn_id"]}
    ]
  }'
);

变更数据捕获与分析

Changelog View

Iceberg提供了变更数据捕获(CDC)功能,可通过创建Changelog视图来跟踪记录级别的变更:

-- 创建变更日志视图
CALL catalog.system.create_changelog_view( 
  table => 'my_db.my_table',
  options => map(
    'start-snapshot-id', '4816648710583642722',
    'end-snapshot-id', '2557325773776943708'
  )
)

Changelog View 与回顾窗口的区别

虽然两者都与历史数据相关,但目的和工作方式不同:

特性 create_changelog_view 回顾窗口优化
目的 变更数据捕获 (CDC) 查询性能优化
输出 创建视图,显示记录变更 不创建任何对象,优化查询计划
使用场景 数据审计、增量同步 时间旅行查询、增量处理性能优化
操作级别 记录级别的变更 文件级别的优化
配置方式 存储过程调用 表属性设置

两者的关联

回顾窗口优化可以提升Changelog View查询性能:

  • 当查询Changelog View时,底层引擎需要扫描相关文件
  • 如果表启用了回顾窗口优化,查询会更高效地确定哪些文件需要读取
  • 尤其在大型表上查询长时间范围的变更时,性能提升显著

分支和WAP组合使用

分支和WAP结合使用可以提供强大的测试和开发能力:

单独使用分支 vs. 分支搭配WAP

特性 单独使用分支 分支搭配WAP
事务范围 单个DML语句 多个DML语句可组合为一个事务
可见性 操作后立即可见 只有在事务提交后才可见
原子性 语句级原子性 事务级原子性
回滚能力 需要使用快照管理函数 可以简单地回滚整个事务
适用场景 简单测试 复杂、多步骤测试

分支+WAP使用示例

-- 设置WAP ID
SET spark.wap.id = 'test-branch-wap-123';

-- 在分支上执行多个操作
INSERT INTO my_table FOR BRANCH 'test-branch' SELECT * FROM source_1;
UPDATE my_table FOR BRANCH 'test-branch' SET col1 = 'new_value' WHERE id = 5;
DELETE FROM my_table FOR BRANCH 'test-branch' WHERE status = 'expired';

-- 提交或回滚整个事务
CALL catalog.system.commit_transaction('test-branch-wap-123');
-- 或
CALL catalog.system.rollback_transaction('test-branch-wap-123');

多表测试与并发验证

虽然WAP事务对每个表是独立的,但可以在同一事务中对多个表使用相同的WAP ID,实现跨表的逻辑事务:

-- 使用同一个WAP ID处理多个表
SET spark.wap.id = 'multi-table-transaction-123';

-- 对第一个表的操作
INSERT INTO table1 FOR BRANCH 'test-branch' VALUES (...);

-- 对第二个表的操作
UPDATE table2 FOR BRANCH 'test-branch' SET col1 = 'new_value' WHERE id = 5;

-- 提交整个事务(影响两个表)
CALL catalog.system.commit_transaction('multi-table-transaction-123');

WAP还可用于测试冲突解决策略:

-- 测试不同的冲突解决模式
CALL catalog.system.cherrypick_snapshot(
  'my_table', 
  'main-branch', 
  snapshot_id,
  map('cherry-pick.conflict-resolution-mode', 'PRESERVE_SOURCE')
);

实用查询示例

时间旅行查询

-- 按版本号查询历史数据
SELECT * FROM my_table VERSION AS OF 5347521240857739485;

-- 按时间点查询历史数据
SELECT * FROM my_table FOR TIMESTAMP AS OF '2025-05-20 10:00:00';

增量处理模式

-- 获取最近24小时的变更
SELECT * FROM my_table AS OF TIMESTAMP current_timestamp()
EXCEPT 
SELECT * FROM my_table AS OF TIMESTAMP current_timestamp() - interval 1 day;

性能监控

-- 监控小文件问题
SELECT count(*) as small_files_count 
FROM my_table.files 
WHERE file_size_in_bytes < 1024*1024;

-- 识别需要压缩的分区
SELECT partition, file_count 
FROM my_table.partitions 
WHERE file_count > 50
ORDER BY file_count DESC;

文件不变性原则

不变性设计基础

Iceberg 的核心设计原则之一是不变性(immutability):一旦写入,文件就不会被修改。这意味着:

  • 文件一旦写入就永远不会被修改
  • 所有"更新"操作实际上是创建新文件,而不是修改现有文件
  • 删除操作也不会真正删除文件,而是通过元数据更新来实现逻辑删除

数据文件操作机制

数据文件(Data Files)在以下情况下会新增:

  1. INSERT 操作

    • 新写入的数据会创建新的数据文件
    • 每个新文件都有唯一的名称(通常包含UUID)
  2. UPDATE 操作

    • 对于 Copy-on-Write 模式:创建包含更新后全部记录的新文件,原文件被逻辑删除
    • 对于 Merge-on-Read 模式:创建包含变更的"删除文件"(delete files)和"插入文件"(insert files)
  3. DELETE 操作

    • 对于 Copy-on-Write 模式:创建不包含已删除记录的新文件,原文件被逻辑删除
    • 对于 Merge-on-Read 模式:创建包含要删除记录位置信息的"删除文件"
  4. MERGE 操作

    • 组合了 UPDATE 和 INSERT 的行为
    • 创建新的数据文件包含新增和更新的记录
  5. 文件压缩/重写

    • 小文件合并会创建新的、更大的文件
    • 原始小文件被逻辑删除

Manifest 文件操作机制

Manifest 文件在以下情况下会新增:

  1. 数据文件变更

    • 当新的数据文件被添加
    • 当现有数据文件被逻辑删除
    • 通常每个提交操作都会创建新的 manifest 文件
  2. Manifest 合并

    • 当 manifest 文件数量过多时,会触发合并
    • 创建新的、合并后的 manifest 文件
    • 原始 manifest 文件被逻辑删除
  3. 显式重写操作

    CALL catalog.system.rewrite_manifests(
      table => 'my_table',
      strategy => 'binpack'
    )
    

Merge-on-Read vs Copy-on-Write

两种模式处理文件的方式有重要区别:

Copy-on-Write (CoW)

  • 每次写操作都会重写受影响的整个文件
  • 读取操作简单,只需读取当前文件
  • 示例:更新100条记录中的1条,需要创建包含100条记录的新文件

Merge-on-Read (MoR)

  • 写入增量更改(删除文件 + 插入文件)
  • 读取时需要合并基础文件和增量文件
  • 示例:更新100条记录中的1条,只需创建包含1条删除和1条插入的小文件

文件生命周期示例

以下是一个 UPDATE 操作的完整流程示例:

初始状态:
- 数据文件: data-file-001.parquet (包含100条记录)
- Manifest: manifest-001.avro (引用 data-file-001)
- 快照: snapshot-001 (引用 manifest-001)

执行更新操作 (Merge-on-Read 模式):
UPDATE my_table SET col1 = 'new_value' WHERE id = 5;

更新后状态:
- 数据文件: 
  * data-file-001.parquet (不变,保留原始数据)
  * delete-file-002.parquet (标识 id=5 的记录被删除)
  * data-file-003.parquet (包含更新后的 id=5 记录)
- Manifest:
  * manifest-001.avro (不变,但在新快照中不再使用)
  * manifest-002.avro (引用所有三个数据文件,并标记它们的状态)
- 快照: 
  * snapshot-001 (不变,但不再是当前快照)
  * snapshot-002 (新的当前快照,引用 manifest-002)

不变性带来的优势

Iceberg 的不变性设计有几个关键优势:

  1. ACID 事务保证:所有更改都是原子性的
  2. 时间旅行能力:可以查询任何历史版本
  3. 并发控制:多个写入可以同时进行,无需复杂锁
  4. 回滚能力:可以轻松恢复到任何历史版本
  5. 缓存一致性:文件不变使缓存更有效

元数据文件管理与提交优化

元数据文件编号与锁优化

Iceberg 元数据文件使用简单的序列号命名(如 v1.metadata.json, v2.metadata.json),这是为了最小化锁持有时间的核心设计:

  1. 乐观并发控制模型

    • Iceberg 采用乐观并发控制(OCC)而非悲观锁定
    • 表更新只涉及单个指针的原子性替换
  2. 提交过程的步骤

    • 获取锁:获取表的元数据锁(毫秒级操作)
    • 原子性更新元数据指针:从v4指向v5(快速操作)
    • 释放锁:立即释放锁,允许其他事务继续
    • 异步删除旧文件:在后台清理旧元数据和数据文件
  3. 延迟删除优化

    • 启用 write.metadata.delete-after-commit.enabled=true
    • 将删除操作从关键提交路径中移除
    • 进一步减少锁持有时间

这种设计使得 Iceberg 在高并发环境中表现优异,同时保持强一致性保证。

总结

Apache Iceberg 提供了强大的功能来管理和优化数据湖表:

  1. 通过元数据表提供表的完整历史和详细信息
  2. 支持分支和快照管理,便于复杂协作开发
  3. WAP协议确保写入操作的原子性和隔离性
  4. 回顾窗口优化提高查询性能
  5. 不变性设计原则保证数据一致性和可靠性
  6. 完整的数据生命周期管理确保存储效率
  7. 精心设计的元数据管理实现高并发性能
  8. 完善的维护操作支持长期稳定运行

为获得最佳性能和可靠性,应定期执行维护操作,并根据工作负载特点优化表配置。