Skip to content

Efficient Column Update 與 Column Families:Iceberg 對 AI/ML Wide Table 的回應

TLDR

After reading this article, you will learn:

  • 為什麼 Iceberg 現有以 row 為單位的 update 機制,在 AI/ML feature engineering 帶來的 wide table 場景下會變得非常昂貴
  • Iceberg 正在討論的 Flip the Axis 與 Column Families 是什麼,為什麼這個方向同時涵蓋了 column-level update 與 layout flexibility 兩種應用
  • 為什麼這個方向值得期待,以及它跟新一代 table format(例如 LanceDB)的競爭,對 Iceberg 接下來的位置意味著什麼

Efficient Column Updates and Column Families in Apache Iceberg

Gabor 與 Peter 都在 Microsoft 的一個研究小組工作,大部分時間都投入在開源 Apache Iceberg 的開發上。他們在 2026 Iceberg Summit 完整介紹了一項仍在早期階段的提案:Efficient Column UpdatesColumn Families。這個提案要回答的問題是,當 AI/ML feature engineering 讓 Iceberg table 動輒擁有上萬個 column、而且需要頻繁地以 column 為單位更新時,現有以 row 為單位設計的 Copy-on-Write 與 Merge-on-Read 都會變得非常昂貴。提案的核心想法是 Flip the Axis:把 update 操作改以 column 為單位進行;而 column-level 拆分一旦成立,這個機制就能繼續推廣到 cache、cold storage、access control 等更多 layout 用途。

我之所以特別想分享這場 talk,是因為雖然自己才剛開始在工作上接觸這塊,就已經實際遇到相同的問題。每次想為一張既有的 wide table 加一個新 column,或更新某個 feature 的計算結果,整張 table 都得重新寫一遍。這個寫入動作的成本會直接反映在 compute 用量與 object storage 的存取費用上,當 column 數量越多、更新頻率越高,這筆成本就越難忽略。talk 裡提到「寫入時間縮短 90%」的數字,背後對應到的就是這筆成本的縮減。

AI/ML Feature Engineering 帶給 Iceberg Wide Table 的三個挑戰

Lakehouse 一開始的願景就是要在同一份資料上同時支撐 BI、analytics 與 AI/ML 等各種 workload,但 Iceberg 的現有設計在那時並沒有把 AI/ML feature engineering 的需求一起納入。在 ML training pipeline 裡,每個 column 都對應到一個 feature,而 feature engineering 這件事本身就需要持續地加新 column、更新舊 column;這種以 column 為單位的 update pattern,跟 Iceberg 為 row-level update 預設的 Copy-on-Write 與 Merge-on-Read 並不對齊。當 ML workload 越來越常見,Iceberg 在 Lakehouse 願景裡原本應該支援的 AI/ML 場景上,就出現了三個具體的挑戰。

需要以 Column 為單位頻繁更新 Feature

ML 模型輸入的每一個 feature,本質上就是 table 裡的一個 column:可能是某個聚合數值(例如使用者在過去 30 天的消費總額)、某個 embedding 向量、或是某個機率分數。這些 feature 都有時效性,例如「過去 7 天消費總額」每天的數值都不同,舊值不再有意義,必須整批換成新算好的值。

對 Iceberg 而言,這意味著 update 的單位從以前的「整 row 換掉」變成「整個 column 換掉」。原本以 row 為單位設計的 Copy-on-Write 與 Merge-on-Read 在這個情境下就會變得相當昂貴:為了更新 1 個 column,要把與它寫在同一個 data file 裡的 999 個 column 都讀進來再寫出去,沒有任何一行資料的 row identity 改變。

面對上萬個 Column 的 Wide Table 規模壓力

特徵工程很容易讓 column 數量一路成長到上千、上萬個,這在傳統 BI 的 table 上幾乎見不到。例如一張 user profile table,可能會同時存放數百個 categorical feature、數百個 aggregate feature,再加上一兩個 dimension 為 512 或 1024 的 embedding 向量。

這個規模直接放大了前一個挑戰的代價。當 column 數量從 50 變成 10,000,每次「更新 1 個 column 就要重寫整個 data file」的 write amplification 也跟著線性放大,連帶把運算成本與儲存成本都提升一個數量級。

遵循快速迭代的實驗節奏

Feature engineering 本質上是一個實驗性的過程:今天試一個新的 time window,明天試一組新的 categorical encoding,後天又想把某個 aggregate 換成另一種演算法。每一次嘗試都會產生新的 column,每一次驗證都可能需要刷新一批舊的 column。

這種節奏代表 Iceberg 在這個場景下面對的每一次 update,都是高頻率、小規模、且 column 維度會持續往上成長的型態。在這種節奏下,每一次 update 的開銷都會被乘上實驗次數,最終決定了團隊一天能跑幾輪實驗、能驗證多少個 hypothesis。

重新檢視現有 CoW 與 MoR 的不足

Iceberg 目前處理 update 的兩種策略 Copy-on-Write 與 Merge-on-Read,本來都是針對 row-level update 設計的:你修改某一筆 row 上的某個欄位,CoW 會把整個 data file 重寫一次、MoR 則會把舊 row 標記成 deleted、把新 row 寫到另一個 file。這套以 row 為單位的設計,在 narrow & deep table 上面對的更新通常也都是 row-level 的,運作得很順。可是一旦把 update 換成「整個 column 換掉」的情境,這兩種策略就會把成本花在不該花的地方。

Copy-on-Write 的寫入放大

承接上一節的場景,假設你有一張 wide table 共 1000 個 column,現在只想把 col5 換成新算好的值。Copy-on-Write 的語意是「把更新後的內容寫成新的 data file,舊的 data file 整份淘汰」,所以它不會只寫 col5,而是會把這個 data file 裡所有 row、所有 column 全部重新讀出來、計算出新版的 col5,再把完整的 1000 個 column 寫回一份新的 data file。

這意味著為了動 1 個 column,要付出 1000 個 column 的寫入成本。如果 table 規模是 N 個 data file,這份 write amplification 會隨 N 線性放大。對於每天可能要更新數十個 feature 的 ML pipeline 而言,這個成本是難以接受的。

Merge-on-Read 的讀取開銷

Merge-on-Read 降低寫入成本的方式,是讓寫入只負責產生兩樣東西:一個 position delete file(標記哪些 row 在 base file 的位置已經被淘汰),以及一個只含更新後 row 的新 data file。代價則是 query 時的讀取邏輯變複雜:reader 必須同時讀 base file、套用 position delete、再 merge 新 data file,才能還原出當下這份 table 的內容。

問題是這套機制依然是 row-centric 的。即使你只更新了 col5,新寫出來的 data file 仍然得帶上完整的 1000 個 column,因為 stitch 的單位是 row,少了任何 column 都會讓 row 不完整。寫入端的 write amplification 沒有真的消失,只是部分轉嫁到了讀取端;並且每多累積一份 delete file,後續每一次 query 的 merge 成本就會跟著上升。

Flip the Axis:用 Vertical Split 重新思考更新

如果以上挑戰的共同根源都是「以 row 為單位處理 column-level update」,那很自然的下一個問題就是:能不能把這個軸線翻轉過來,讓 update 從一開始就以 column 為單位進行?這場 talk 的核心提案 Flip the Axis 就是這條路的具體實踐:透過 Vertical Split,把 update 行為從橫向的「整 row 換掉」改成縱向的「只寫被動到的那幾個 column」,並把跨檔案的 stitch 邏輯延後到讀取時才處理。

關於這個方向的具體成效,talk 後段分享了一份與 Apple 團隊合作的 Spark POC,分別針對寫入端的節省與讀取端的代價做了量測,下面引用的數字都來自這份 POC。

只寫入被更新的 Column Update File

承接 CoW 重寫整份 data file 的情境,假設你執行 UPDATE tbl SET col1 = concat('new', id),過去得把所有 row、所有 column 都重寫一遍。Vertical Split 想做的事情很簡單:既然只有 col1 的值會變,那麼產出的檔案就只該包含 col1,其餘 999 個 column 完全不必碰。

實際的 write path 大致是這樣:scan 階段只讀計算 col1 所需的最小欄位集合(在這個例子裡是 _file_pos_partitionid),算出新的 col1 之後,依照 _file shuffle 分組、依照 (_file, _pos) 排序,最後為每一份原始 data file 各寫出一份只含 col1 的 column update file。寫完之後再透過類似下面這段 API 把 mapping 一次 commit 到 metadata:

table.newColumnUpdate()
  .withFieldIds(fieldIds)
  .addColumnUpdate(FILE_A, updateFileA)
  .addColumnUpdate(FILE_B, updateFileB)
  .commit();

從寫入端的角度看,原本「動 1 個 column 等於寫 1000 個 column」的 write amplification 直接降為「動 1 個 column 就只寫 1 個 column」。對應到 POC 的數字,在 R1W1(輸入 1 個 column、輸出 1 個 column)的情境下,寫入時間縮短超過 90%;即使提高到 R20W20,也仍能省下約 75%。

用 Composite Reader 在讀取時 stitch

承接寫入端的設計,讀取端要解決的問題就變得明確:當一份 table 的某些 column 散落在 base file、某些 column 散落在多份 column update file 時,reader 必須能在 query 執行階段,把這些散落的 column 重新組合回完整的 row。Iceberg 在這條路上引入的關鍵元件是 Composite Reader

SELECT col1, col2, col4 FROM tbl 為例,Composite Reader 會先讀 metadata 來判斷每個 column 的來源:col1 沒被更新,從 base file 讀;col2 被某份 update file 覆寫過,要去那份 update file 讀;col4 完全是新增的,只存在另一份 update file 裡。確定好來源之後,Composite Reader 會為每個來源建立各自的 reader、平行讀出資料,再用一個 row identity(例如 __row_id_file + _pos)把同一個邏輯 row 的 column 對齊、stitch 成最終結果。對 query 端而言,整個過程看起來跟讀一份普通的 Parquet 檔案沒有差別。

處理 Row Group 對齊帶來的讀取開銷

讀取端的主要成本來自底層 Parquet 結構不對齊。stitch 邏輯本身相對便宜,真正昂貴的是 row group 邊界無法在 update file 上對齊:base data file 通常很大,例如 512MB,內部會被切成多個 row group 以方便平行讀取;column update file 則通常很小,往往只有一個 row group。當 reader 想要平行掃 base file 後半段的 row group 時,它在 update file 裡找不到對應的起始 offset 可以直接 seek 過去,必須先從 update file 開頭一路遍歷下去,才能定位到正確的縫合點。

POC 也量出讀取端的代價:當 query 只需要 stitch 一份 column update file 時,整體讀取會多出約 10% 的開銷;當 update 累積到 20 份 column update file 時,這個開銷會上升到約 35%。Talk 裡也提到這部分的優化方向,例如善用 dictionary encoding 讓 update file 進一步縮小,或從 file format 層級提供更友善的 offset metadata。整體看來,Vertical Split 把 column-level update 的寫入成本降低了一個量級,代價是讀取端被引入了一個目前在 10–35% 量級、且仍有改善空間的 overhead。

Column Families:重新定義的 Layout 設計

到這裡為止,Vertical Split 都還只是一個讓 column-level update 變便宜的機制。Talk 後半段由 Peter 接手,他提出的觀察是:既然系統已經能把同一張 table 的不同 column 物理拆到不同檔案、再於讀取時 stitch 回完整 row,那這份能力其實也可以被當作一種 layout 工具,不必只用在 update 場景上。Peter 把這個概念稱為 Column Families,讓使用者依照 access pattern 主動把 column 分群、選擇要落在什麼樣的檔案上,並以此推導出下面幾種具體的用法,最後再透過一份微基準測試來驗證整體可行性。

提升讀取效能

承接 Vertical Split 的能力,假設一張 user profile table 有 1000 個 column,但日常 query 真正會碰到的可能只是其中幾十個 hot column(例如使用者 ID、年齡、最近一次活躍時間、目前等級等)。在原本的單檔案 layout 下,這些 hot column 與其他冷門 column 物理混雜在同一份 Parquet 檔案裡,cache 也只能粗粒度地以整份檔案為單位:要嘛整份載入 cache,要嘛完全沒上 cache。

把這些 hot column 獨立成一個 column family、物理上拆到專屬的檔案後,cache 層就可以只服務這個小而集中的子集:檔案大小通常只有原本的幾十分之一,能完整放進 cache;其他不需要 cache 的 column 則自然落在比較慢、比較便宜的 storage 上。query 在大多數情況下只需要打到 hot column 檔案,cache hit rate 上升,整體 latency 自然下降。

降低存儲成本

對應 hot column 的另一面是 cold column:某些 audit log、debug-only metadata、或久遠的歷史 feature,可能一個月只被讀到幾次,卻佔據了不小的儲存體積。如果它們混在主要的 Parquet 檔案裡,整份檔案就只能放在比較昂貴的高速 storage tier 上,等於是讓不常用的資料一起付高 storage 費用。

把 cold column 獨立成一個 column family 檔案後,這份檔案就可以直接放到便宜的 cold storage(例如 S3 Glacier 或對應的低頻存取等級),只在真的需要時才付出 retrieval 成本,平常的 storage 帳單則大幅下降。當 wide table 規模一旦進到 TB、PB 等級,這種冷熱分層帶來的成本節省會非常可觀。

增強安全控管

身分證字號、信用卡號、銀行帳戶這類敏感欄位,在傳統 Parquet layout 下會跟其他 column 一起寫在同一個檔案裡。即使上層 catalog 或 query engine 有 column-level access control,底層只要任何流程能讀到那份檔案,技術上就有可能解析出敏感欄位:query engine 漏洞、或某段 log 不小心 dump 整個 row,都可能成為突破口。

Column Families 讓這類欄位可以被指定為一個獨立的 column family,物理上落在一個只有特定身份才能讀的檔案夾或 bucket 內,並由 object storage 既有的 file-level ACL 處理權限控管。一般的 ML training pipeline 連那份檔案的 read permission 都沒有,從物理層級就排除了存取的可能;同時 audit log 上也能直接看到誰存取了 sensitive column family,合規回報相對單純。

改善壓縮效率

Parquet 的壓縮機制是以 column chunk 為單位,當同一份檔案裡混雜了寬度差異極大的 column(例如只有 4 byte 的 integer,與動輒幾 KB 的長字串或 JSON blob),壓縮演算法很難對所有 column 都做到理想的壓縮率。寬的 column 會佔掉大部分檔案空間,連帶讓整份檔案的 metadata 變得龐大;窄的 column 即使有重複模式可以高度壓縮,也享受不到同等程度的編碼最佳化。

把寬度相近、資料模式接近的 column 分成同一個 column family、各自寫到專屬的檔案,每份檔案內部的 column 就會比較同質,編碼與壓縮選擇也比較容易最佳化。不過 Peter 也誠實指出,總體壓縮率最終仍然會取決於實際資料模式:在完全隨機的資料上,把一張 10,000 個 column 的 table 拆成 10 個各 1,000 個 column 的 column family 檔案,總大小跟維持單檔基本不會差太多;只有當資料本身具備可以被壓縮的 pattern 時,異質性切開帶來的差距才會明顯。使用者必須自行 benchmark 過才知道在自家資料上是否真的划算。

驗證整體可行性

除了上面四個應用場景,Peter 還針對 Column Family 整體的讀寫成本做了一份微基準測試:把一張 10,000 個 column 的 table 切成 1、2、5、10 個 column family,分別量測單執行緒與多執行緒讀取下的表現。結論是這套設計在不同 compute 環境下的表現落差非常明顯。

在單執行緒模式下,Column Family 的讀取效能與原始 Iceberg 大致相當:stitch 帶來的額外開銷剛好被 metadata 變小帶來的節省抵銷掉。把讀取改成平行(每個 column family 各自一個 reader)後,微基準測試裡的效能可以提升 80–90%。但同樣的設計在 Spark 上實測時,效能卻反過來下降了 30–40%,原因是測試環境的 CPU 核心數不足,平行 reader 競爭核心反而造成額外的 scheduling 開銷。這個結果也反映了一件事:Column Family 要在分散式 compute engine 上真正發揮潛力,前提是底層有足夠的並行運算資源,這也是社群接下來需要繼續處理的方向。

Next Steps

整體看下來,Efficient Column Update 與 Column Families 是一個我非常看好的方向。它把 column-level update 與 layout flexibility 直接做進 table format 的底層,而不是靠應用層或 compute engine 去迴避 row-centric 的限制。這是 Iceberg 在 AI/ML workload 上真正補齊 Lakehouse 願景的關鍵一步。

值得對照的是,新一代的 table format 例如 LanceDB 早就把這類 column-level 的能力做成核心特色之一。它原生就以 column 為單位處理 update、cache 與儲存策略,這也是 LanceDB 在目前 lakehouse 競爭格局裡成長最快、最多人看好的原因之一。當 AI/ML 工作流逐漸主導 lakehouse 上的 read/write pattern,誰能把 column-level 的成本降到合理區間,誰就會被選為新一代基礎建設的預設選項。

面對這樣的壓力,期待 Iceberg community 能盡早把 Efficient Column Update 與 Column Families 納入正式版本。這對既有使用者是一個明顯的改善,也是 Iceberg 維持 Lakehouse 主流地位的必要動作。

Comments