Hudi 时间旅行与增量查询的区别分析

2026/05/12

当前文章整理关于 Apache Hudi 时间旅行(Time Travel)、增量查询(Incremental Query)、压缩(Compaction)和清理(Cleaner)的关键内容,重点说明它们的语义区别、底层关系,以及对历史版本可用性的影响。

1. 主要区别

时间旅行和增量查询都依赖 Hudi 的 Timeline、MVCC 和 File Slice 机制,但查询目标不同:

功能 核心问题 返回结果
时间旅行 某个历史时刻,整张表长什么样? 指定 instant 对应的完整快照
增量查询 某个时间范围内,哪些记录发生过变化? 指定 begin/end 范围内的变更记录

简单理解如下:

2. 时间旅行

时间旅行通过 as.of.instant 指定一个历史 instant,查询表在该 instant 的逻辑状态。

代码示例:

val df = spark.read.format("hudi")
  .option("as.of.instant", targetInstant)
  .load(basePath)

典型用途如下:

时间旅行返回的是目标 instant 下的完整表快照,而不是变化记录。

3. 增量查询

增量查询通过 begin/end instant 指定一个时间范围,只读取该范围内发生变化的数据。

示例代码:

val df = spark.read.format("hudi")
  .option("hoodie.datasource.query.type", "incremental")
  .option("hoodie.datasource.read.begin.instanttime", beginCompletionTime)
  .option("hoodie.datasource.read.end.instanttime", endCompletionTime)
  .load(basePath)

典型用途如下:

增量查询默认通常返回增量范围内每个 key 的最新状态,不一定等同于完整 CDC 明细;如果需要完整 insert/update/delete 事件,需要结合 Hudi CDC 能力和对应配置。

4. Hudi 1.0.x 中的 requested time 与 completion time

在 Hudi 1.0.x 的 timeline layout v2 中,需要区分两个时间:

时间 含义 常见位置
requested time instant 被请求创建的时间 记录字段 _hoodie_commit_time 通常是这个值
completion time commit 真正完成的时间 timeline 中 HoodieInstant.getCompletionTime

关键点:

推荐通过 HoodieTableMetaClient 获取 completed timeline:

case class CommitTime(requestedTime: String, completionTime: String)

val commits = metaClient.getCommitsTimeline
  .filterCompletedInstants()
  .getInstants
  .asScala
  .map(instant => CommitTime(instant.requestedTime(), instant.getCompletionTime))
  .toSeq

5. Compaction 与查询功能的关系

Compaction 不是时间旅行,也不是增量查询。它是 MOR 表的 table service,用于把 log files 合并进新的 base parquet 文件。

关系可以理解为:

Timeline / MVCC / File Slices
├── Time Travel       按某个 instant 读取历史快照
├── Incremental Query 按 instant 范围读取变化
└── Compaction        合并 MOR 表的 base + log,生成新的 base file

Compaction 的主要作用:

6. COW 与 MOR 的底层差异

6.1 COW 表

COW 是 Copy-on-Write。更新时直接生成新的 parquet base file。

示例:

t1: base parquet v1: A=10
t2: update A=20
    生成 base parquet v2: A=20
    旧 base parquet v1 暂时保留

特点如下:

6.2 MOR 表

MOR 是 Merge-on-Read。更新通常先写入 log file,后续可通过 compaction 合并成新的 base file。

示例:

t1: base parquet: A=10
t2: log file: A=20
t3: compaction
    生成新的 base parquet: A=20
    旧 base/log 暂时保留

特点如下:

7. Compaction 和时间旅行、增量查询的关系

7.1. Compaction 和时间旅行的关系

Compaction 通常不会影响时间旅行功能。

因为 Compaction 一般不是原地覆盖旧文件,而是生成新的 file slice。旧的 base/log 文件通常会暂时保留,用于支持 MVCC、长查询和历史版本读取。

例如:

t1: A=10
t2: A=20
t3: B=30
t4: compaction

Compaction 后可能形成:

旧 file slice:
  base/log 可以重建 t1/t2/t3 的历史状态

新 file slice:
  compacted base file,包含 t4 时刻的 A=20, B=30

所以此时:

as.of.instant = t1 -> 理论上仍可看到 A=10
as.of.instant = t4 -> 看到 A=20, B=30

所以真正可能让 as.of.instant = t1 不可用的是 cleaner 或 timeline/file retention,而不是 compaction 本身。

7.2. Compaction 和增量查询的关系

Compaction 通常不会改变增量查询的逻辑结果。

前面提到过 Compaction 会把文件变更合并到 base file,但是原来的文件并不会立即删除,所以增量查询仍然可以按照 timeline 的 instant 范围判断哪些记录发生变化。

但 compaction 可能影响读取的文件路径、查询性能以及 Read Optimized Query 的可见数据范围。

但是如果 cleaner 已经清理了相关的历史文件,那么增量查询就会出现问题。

8. Cleaner 参数决定历史版本保留问题

Cleaner 参数负责删除超过保留策略的旧文件版本。

所以 Compaction 和 Cleaner 的区分如下:

如果 cleaner 还没有清理支持 t1 的旧文件,那么指定 as.of.instant = t1 就可以查询历史版本。

如果 cleaner 已经清理了支持 t1 的旧文件,那么查询 t1 的版本就会出现失败。

COW 和 MOR 都受 cleaner 管理,对比如下:

表类型 是否有 compaction 是否有 cleaner 历史文件清理
COW cleaner
MOR cleaner

9. 时间线示例

假设有一张 MOR 表时间线如下:

t1 delta/base: A=10
t2 delta:      A=20
t3 delta:      B=30
t4 compaction: 生成新 base: A=20, B=30
t5 delta:      B=40

时间旅行结果为:

as.of t1 -> A=10
as.of t2 -> A=20
as.of t3 -> A=20, B=30
as.of t5 -> A=20, B=40

增量查询结果:

begin=t1, end=t3 -> 返回 t2~t3 范围内变化的数据
begin=t3, end=t5 -> 返回 t5 范围内变化的数据,例如 B=40

Compaction t4 的作用是在压缩前 Snapshot 可能需要读取 base + 多个 log,但是压缩后 t4 之后的读取可以更多依赖新的 compacted base file。

t4 之后 t1 的历史语义不会立即消失,t1 是否还能查,取决于文件是否仍在 cleaner 策略保留范围内。

10. 常见理解的问题

10.1 Compaction 后旧数据不一定立刻消失

Compaction 会生成新的合并后 base file,但旧 base/log 通常由 cleaner 后续按策略清理。

10.2 MOR 和 COW 的历史版本问题

COW 更新时也会生成新的 base file version,旧版本也可能暂时保留,因此也可以支持时间旅行。区别是 COW 没有 compaction。

10.3 增量查询的边界问题

在 Hudi 1.0.x 中不推荐使用 _hoodie_commit_time。因为 _hoodie_commit_time 通常是 requested time,而增量查询 begin/end 边界应优先使用 timeline 中的 completion time。

10.4 时间旅行不能查询任意历史版本

时间旅行能力受 cleaner、archival 和文件保留策略影响。旧文件被清理后,对应历史快照可能无法重建。

11. 生产环境配置

11.1 依赖时间旅行

如果依赖时间旅行比较强,需要配置足够的历史保留:

hoodie.cleaner.commits.retained
hoodie.keep.min.commits
hoodie.keep.max.commits

建议配置如下:

11.2 依赖增量查询

如果对增量查询依赖比较多,需要管理好时间线参数,重点如下:

11.3 如果使用 MOR 表

需要同时关注 compaction 和 cleaner,compaction 主要影响查询性能和 read optimized 可见性,cleaner 影响历史版本和增量查询可追溯性。

建议配置通过 compaction 控制 log 文件数量,通过 cleaner retention 控制历史可查时间。

注意不要把 compaction 误认为历史清理机制。

综上,对时间旅行和增量查询的主要区别总结如下。

  1. 时间旅行查询历史快照。
  2. 增量查询读取时间范围内的变化。
  3. Compaction 优化 MOR 表的物理文件布局。
  4. Cleaner 决定旧版本文件保留多久。
  5. COW 没有 compaction,但同样有 cleaner 和历史 file versions。
  6. MOR 有 compaction,也有 cleaner。
  7. 历史版本是否还能查,核心取决于 timeline 和所需文件是否仍在保留范围内。