1038 字
5 分钟
在 kind 上自建 Lakehouse(二):五引擎共写一张 Iceberg 表,跨引擎读写与 positional delete 合规性实测

这是「自建 Lakehouse 实战」系列第二篇。第一篇把五大引擎都接到了同一张 Lakekeeper/GCS 上的 Iceberg 目录。本篇问一个直接的问题:**它们写出来的表,彼此读得动吗?**尤其是涉及行级删除(merge-on-read)时。 这正是 Iceberg “开放表格式、引擎无关”承诺的试金石。结论先放这儿:绝大多数组合都能互读,唯独 Doris 在做了 UPDATE/DELETE/MERGE 之后写出的表,Trino 等严格引擎读不了——根因是 Doris 的 positional delete 文件不符合 Iceberg 规范。下面是完整过程。

一、各引擎的原生 CRUD + MERGE#

四个引擎都建 format-version=2 的 Iceberg 表,跑 INSERT / SELECT / UPDATE / DELETE / 三分支 MERGE INTO:

引擎INSERT/SELECTUPDATE/DELETEMERGE INTO路径风格
Trino 481✅(三分支全过)gs://
Spark 3.5.8✅(需第一篇的 4 个修复)gs://
Doris 4.1.1✅(需 4.1.0+)✅(三分支全过)s3://
StarRocks 4.1.1DELETE ✅ / UPDATE 受限❌(V2 MERGE 仍在开发,#73684)

Trino、Spark、Doris 的行级 DML 都完整可用;StarRocks 的 DELETE 能产出 merge-on-read 删除文件(这就够验证合规性了),但 UPDATE 在 4.1.1 上报”does not support update”,MERGE INTO 还没合并。

二、跨引擎读矩阵#

把上面写出来的表互相读一遍,得到下表(同一张 Lakekeeper 目录、同一个 GCS 桶):

表的来源Trino 读Doris 读Spark 读StarRocks 读
Trino(含 MERGE 删除)
Spark(含 MERGE 删除)
StarRocks(含 DELETE 删除)
Doris —— 纯 INSERT
Doris —— 含 UPDATE/DELETE/MERGEposition is null❌*❌*

* Trino 的失败是实测确认的;Spark、StarRocks 同样按 field-id 读取删除文件,机理一致,预期同样失败。Doris 能读自己的表,是因为它按列名而非 field-id 读取。

三、根因 的 positional delete 文件缺少保留 field-id#

这个 bug 值得展开,因为它非常隐蔽。把出问题的删除文件从 GCS 下载下来,用 pyarrow 检查 parquet 的 field-id,真相才浮出水面:

  • Iceberg 规范规定,positional delete 文件的两列必须带保留 field-id:file_path = 2147483546pos = 2147483545
  • 引擎读删除文件时,是按 field-id 定位列,而不是按列名
  • Trino 的删除文件 正确。
  • StarRocks 的删除文件 也正确(2147483546 / 2147483545)。
  • Doris 的删除文件:这两列的 PARQUET:field_id 都是 None——Doris 没写保留 field-id。

于是 Trino 按 id 2147483545 去找 pos 列,找不到,返回 null → 报错 position is null。这不是 Trino 的能力缺陷(它能完整读写自己和 Spark 的 merge-on-read 删除),而是 Doris 写出了不符合规范的删除文件。 需要澄清的一点 并不是 copy-on-write。用 SELECT content, file_path FROM "表名$files" 可以确认 Trino 和 Doris 都是 merge-on-read(content=1 即 position delete)。两者唯一的差别就是那对 field-id。 (次要差别 把路径写成 s3://(GCS S3 互操作),Trino 是 gs://——这个靠在 Trino 里也打开 fs.native-s3.enabled 能解决;但 field-id 缺失才是真正的硬障碍。)

四、实践建议#

  • 行级 DML 尽量放在 Trino 或 Spark 做:它们的删除文件是通用可读的。
  • 如果一张 Doris 表需要被其它引擎读,要么保持纯 INSERT(无删除文件),要么让 Doris 走 copy-on-write(write.update.mode/write.delete.mode/write.merge.mode 设为 copy_on_write),这样不产生删除文件。
  • StarRocks 在合规性上是干净的——它写的删除文件 Trino 读得动;只是它自身的 UPDATE/MERGE 能力当前还有限。

小结#

Iceberg “引擎无关”的承诺在这套环境里基本兑现了:四个引擎共用一张表、互相读取大多顺畅。唯一的破绽是 Doris 的 positional delete 不写保留 field-id,导致严格按规范读取的引擎失败——一个仅凭 SELECT 报错很难定位、必须下载文件看 field-id 才能坐实的细节。下一篇换个维度:在读性能上,这些引擎差多少?


📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):

  1. 搭建篇 —— Lakekeeper + GCS + 五大引擎接入
  2. 互操作篇(本文) —— 跨引擎读写、MERGE 与 positional delete 合规性
  3. 性能篇 —— Iceberg 读性能横评 vs Doris vs StarRocks
  4. 联邦篇 —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价
  5. 扩容篇 —— Doris 存算分离 BE 扩容量化:加算力到底快多少
  6. 实时入湖篇 —— Flink CDC / PostgreSQL → Doris 存算分离,延迟与节点数的真相
在 kind 上自建 Lakehouse(二):五引擎共写一张 Iceberg 表,跨引擎读写与 positional delete 合规性实测
https://notes.ezworker.cc/posts/lakehouse-on-kind-2-interop/
作者
jayzhu
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0