mor表适合写入频繁、更新/删除量大且能接受查询稍慢的场景;它通过追加日志实现快速upsert/delete,但查询需实时合并base与log,导致io和cpu压力大、延迟抖动明显。

什么时候该选 MOR 表而不是 COW
MOR(Merge-On-Read)表适合写入频繁、更新/删除量大、且能接受查询稍慢的场景。它把新数据先写进 log 文件,不立刻合并,所以 upsert 和 delete 操作非常快——基本就是追加写日志,没重写开销。
但代价是:每次 SELECT 都得实时合并 base(Parquet)和最新 log,IO 和 CPU 压力明显上升,尤其当 log 文件多或大时,查询延迟抖动明显。
- 适用:实时数仓中高频小批量更新(比如用户行为状态变更、订单状态流转)
- 不适用:对查询延迟敏感、且更新不密集的报表类场景
- 注意:
hoodie.compact.inline默认关闭,不手动触发压缩的话,log 会越积越多,SELECT越来越慢
COW 表的 upsert / delete 实际开销在哪
COW(Copy-On-Write)表每次 upsert 或 delete 都会重写整个受影响的文件组(file group),也就是读旧 Parquet + 应用变更 + 写新 Parquet。这意味着:
- 写放大严重:1 行更新可能触发 MB 级 Parquet 重写
- 并发写容易冲突:
HoodieWriteConfig.UPSERT_OPERATION_OPT_KEY下多个任务同时改同一文件组会失败回退 -
delete不是逻辑标记,而是物理移除 —— 所以必须走完整重写流程,无法跳过 - 好处是:查得快、稳定,因为数据永远是“干净”的 Parquet,无运行时合并
典型错误现象:HoodieIOException: Failed to commit ... because file group X is locked,本质就是 COW 并发写争抢太猛。
MOR 的 delete 为什么比 COW 更“轻”,但又更难预测
MOR 的 delete 只是往对应 log 文件里追加一条 DELETE 标记记录,不碰 base 文件,所以瞬时完成。但它的真实效果要等下一次压缩(compaction)或查询时合并才体现。
- 未压缩前,
SELECT仍可能返回被删行(取决于查询是否启用hoodie.datasource.query.type=realtime) - 如果长期不 compact,log 中 delete 标记堆积,反而拖慢后续所有查询
-
hoodie.cleaner.policy=KEEP_LATEST_FILE_VERSIONS不会清理带 delete 标记的 log,必须靠 compaction 合并后 clean 才真正释放空间
换句话说:MOR 的 delete 是“懒删除”,快是快,但不 compact 就等于没删干净。
实测性能差异的关键变量其实是这些
别只看文档说“MOR 写快查慢”,真实差距取决于三个硬参数:
-
hoodie.parquet.max.file.size:设得太小 → COW 小文件爆炸,写更慢;MOR log 文件变多 → 查询更卡 -
hoodie.compact.inline.max.delta.commits:MOR 不设这个,compact 就不会自动触发,log 无限膨胀 - 集群 shuffle 资源:MOR 查询合并阶段大量依赖 Spark shuffle,
spark.sql.adaptive.enabled=true对它帮助有限,反而是spark.sql.adaptive.coalescePartitions.enabled更关键
最常被忽略的一点:MOR 表在 Flink SQL 下默认用 read_optimized 模式查,根本看不到最新 log —— 得显式指定 read_mode = 'realtime',否则你测的压根不是 MOR 的真实查询延迟。











