跳转至

Time Travel(时间旅行)

Explanation · 原理进阶

一句话理解

查询表过去某一时刻或某一版本的样子。湖仓天生具备——旧 Snapshot 的元数据 + 数据文件还在,就能读回去。Time Travel = 读Rollback / Restore = 写(换当前指针)——这两件事不是一回事,容易混。

TL;DR

  • Time Travel 读一个过去 snapshot,不改当前表
  • Rollback / Restore 把当前指针切到过去 snapshot——是一次新的 commit
  • 能读多久 = snapshot 未 expire + 数据文件未 vacuum(两个条件都得满足)
  • 精度 = commit 频率;一天 commit 一次就没有"早上 10 点"
  • 各家默认窗口差异大:Iceberg 5 天 / Delta 实际 7 天(受 vacuum 约束)/ Paimon 1h / Hudi 按 cleaner.commits.retained 自定义;合规场景必须显式拉长或用 tag 锁定
  • 合规审计:用 tag 固定关键 snapshot,避免被 expire 清走

1. 两种写法

-- Iceberg
SELECT * FROM orders VERSION AS OF 123456789;                     -- by snapshot id
SELECT * FROM orders TIMESTAMP AS OF '2026-03-01 10:00:00';       -- by timestamp
SELECT * FROM orders FOR SYSTEM_VERSION AS OF 123456789;          -- SQL:2011 风格

-- Delta
SELECT * FROM orders VERSION AS OF 42;
SELECT * FROM orders TIMESTAMP AS OF '2026-03-01';

-- Paimon
SELECT * FROM orders /*+ OPTIONS('scan.snapshot-id' = '42') */;

-- Spark / Python (Iceberg)
spark.read.option("snapshot-id", 123456789).table("orders")
spark.read.option("as-of-timestamp", "1735689600000").table("orders")

2. 它为什么存在

Time Travel 不是"为了酷而加的功能",是一系列现实痛点的副产品:

  • Bug 回溯 —— 昨晚的报表对不上,能不能看看"出错那个快照里"数据长什么样?
  • 审计 / 合规 —— 监管要求能回放过去某一时刻的账
  • ML 训练 —— 复现训练结果必须有确定性的数据版本
  • 回滚 —— 一个坏作业写坏了表?ROLLBACK 到上一个 snapshot
  • AB 对照 —— 老查询跑旧版本、新查询跑新版本,对照差异

3. Time Travel vs Rollback / Restore(读 vs 写)

最容易混的一对概念。它们不是一件事

Time Travel Rollback / Restore
本质 写(新 commit)
是否改当前指针
原子性要求 无(只读) 需 CAS
典型命令 VERSION AS OF · TIMESTAMP AS OF rollback_to_snapshot · RESTORE TABLE
撤销难度 无副作用 要再 rollback / restore 一次

各家 restore 语义差异

  • Delta RESTORE:软回滚——生成一个新 commit,内容等同目标 snapshot,不删除中间的 snapshot(仍可 time travel 回来)
  • Iceberg rollback_to_snapshot:切指针——当前 snapshot 指向过去;被跳过的 snapshot 保留到 expire 为止,之后可以被 cleanup
  • Paimon:类似 Iceberg,CALL rollback_to('db.t', 42)

4. Snapshot 选取策略

选哪个 snapshot 读有多种维度:

  • 按 snapshot ID:精确但需要事先知道 ID —— 查 <table>.snapshots<table>.history
  • 按 timestamp:找"早于或等于该时刻的最新 snapshot"——commit 频率决定精度
  • 按 tag(Iceberg only):给关键 snapshot 打 tag(如 release-2026-01),tag 不随 retention 过期
  • 按 branch(Iceberg only):WAP 工作流——开 branch 写、读 branch head、验证后快进合并
-- 查 snapshot 列表
SELECT snapshot_id, parent_id, committed_at, operation, summary
FROM orders.snapshots
ORDER BY committed_at DESC;

-- 打 tag 固定审计点
ALTER TABLE orders CREATE TAG `year-end-2026` AS OF VERSION 123456;

-- 按 tag 读
SELECT * FROM orders VERSION AS OF 'year-end-2026';

5. 和 expire / vacuum 的联动(关键边界)

Snapshot 过期是一个危险操作——会让 time travel 永久失效。关键参数:

系统 保留元数据 保留数据文件 实际 time travel 窗口
Iceberg history.expire.max-snapshot-age-ms(5 天默认)+ min-snapshots-to-keep 同 snapshot 保留 取决于 expire 调度
Delta delta.logRetentionDuration(30 天) delta.deletedFileRetentionDuration(7 天) min(log retention, file retention) ≈ 7 天默认
Paimon snapshot.time-retained(1h)+ snapshot.num-retained.min(10) 同 snapshot 约 1 小时默认(常要拉长)
Hudi hoodie.cleaner.commits.retained 同 commit 自定义策略

重要:Delta 的 log 保留 30 天不等于能 time travel 30 天。VACUUMdeletedFileRetentionDuration 默认 7 天清理数据文件——默认实际 time travel 窗口 ≈ 7 天。要拉长必须同时调这两个。

还有一个关键点:Delta 的 VACUUM 默认不自动执行——要靠外部调度器(Airflow / Databricks Jobs)周期运行。如果从没跑过 VACUUM,数据文件会一直累积,time travel 窗口反而被"意外延长"(但对象存储成本爆炸)。Databricks Runtime 的托管表会自动跑 VACUUM,开源栈需自建。

合规 / 审计场景:用 tag 或 branch 固定关键 snapshot,让它不受 expire 影响。例如年末账、季度末报表、ML 训练数据版本。

6. 相关操作

-- Iceberg · 完整运维
SELECT * FROM orders.history;        -- 所有历史 snapshot
SELECT * FROM orders.snapshots;      -- 含 summary 的详情
CALL system.rollback_to_snapshot('orders', 123);
CALL system.expire_snapshots(
  table => 'orders',
  older_than => TIMESTAMP '2026-01-01',
  retain_last => 100
);
CALL system.remove_orphan_files('orders', TIMESTAMP '2026-01-01');

-- Delta
DESCRIBE HISTORY orders;
RESTORE TABLE orders TO VERSION AS OF 42;
RESTORE TABLE orders TO TIMESTAMP AS OF '2026-03-01';
VACUUM orders RETAIN 168 HOURS;  -- 至少 168h = 7 天

7. 和传统 DB 的差异

传统 DB 的 "as-of query"(Oracle Flashback / SQL Server Temporal Tables)靠 undo 日志 + WAL 回放,成本高、窗口短(通常几分钟到几小时)。

湖仓靠不可变快照序列天然支持长窗口时间旅行,这是湖表格式的"白送功能"——代价是存储(snapshot 不清理会无限膨胀)。

8. 陷阱

  • "VERSION AS OF 昨天" 但 vacuum 已跑 → 数据文件已物理删除 → 读失败
  • 合规要求回到 3 年前 → 没关 expire → 数据早被清 → 应提前建 tag
  • 把 rollback 当 undo → rollback 后又 commit → 原 snapshot 被跳过成孤立节点,再回去需要 rollback 链
  • 流消费者追不上保留窗 → 默认 1h 的 Paimon snapshot retention 对慢消费者不够 → 调 snapshot.num-retained.min 大些
  • Time travel 到 schema 不同的 snapshot → schema evolution 后老 snapshot 的数据按当时 schema 读,新增列返回 null
  • 跨 catalog 的 snapshot id 不通用 → snapshot id 是表级的,不是全局的

9. 相关概念

10. 延伸阅读