lakehouse 是做什麼的?
針對開放資料表格式儲存和 Spark 程式碼提供參考級決策支援。版本和日期請參考 references/landscape-2026-11.md;在做出任何與版本相關的聲明之前,請先檢查該文件。如需 Databricks、Snowflake 或 AWS 服務的具體資訊,請使用 de-vendors 技能及其指向的官方廠商技能。
1. 是否適合使用湖倉?
從最便宜且符合需求的方案開始,僅在當所列出的觸發條件真實存在且可衡量時,才向上移動。
- Postgres(或單一檔案集上的 DuckDB):當資料量低於約 1 TB、僅由一個引擎讀取,且同時進行的分析使用者很少時使用。原因:無需目錄、無需壓縮、無需執行叢集。
- 雲端倉庫:當許多 SQL 使用者需要彈性的並行處理能力,且無人需要對相同資料使用第二個引擎時使用。原因:管理儲存、維護和管理成本已包含在價格中。
- 湖倉:當至少符合以下一項條件時使用:數十 TB 或更多資料;多個引擎(Spark、Trino、DuckDB、Python)必須讀取同一份資料副本;機器學習或基於檔案的工作負載與 SQL 共享資料表;在您的資料量下,物件儲存的儲存成本明顯低於倉庫儲存;開放格式是硬性要求(用於退出、審計、廠商中立)。
- 不要因為一個沒有具體數字的「未來規模」而選擇湖倉。在推薦之前,請詢問資料列數、每日成長量、同時線上的讀取者和引擎數量。
- 對於仍希望使用開放格式的小型和中型資料:使用 DuckLake 或純 Parquet 搭配 DuckDB,勝過執行 Spark。
2. Parquet 配置
- 每個檔案的目標大小為 128 MB 到 1 GB;如果不確定,目標設為 256 MB 到 512 MB。許多檔案小於 32 MB 會導致清單列出和開啟的成本主導查詢。
- 每個檔案中的列群組大小約為 128 MB(未壓縮)(100 萬行以上);較小的群組會削弱最小/最大值修剪和壓縮效果。
- 僅根據查詢會進行篩選的低基數欄位(日期或月份、區域)進行分區。一個好的分區應包含每個寫入批次至少一個目標大小的完整檔案;如果分區平均小於 1 GB,請改用更粗略的粒度或使用叢集。
- 永遠不要根據高基數 ID 進行分區。對這類欄位使用叢集(Delta liquid clustering、Iceberg sort order / Z-order)。
- 根據最常被篩選的欄位對檔案進行排序或叢集,以便最小值/最大值統計資訊能修剪列群組。
- 串流或頻繁的微批次寫入會產生小型檔案;請安排壓縮作業,不要依賴寫入器來調整檔案大小。
3. 選擇資料表格式
根據您已有的引擎和目錄來決定,而不是根據功能清單。
- Delta Lake:當 Databricks 或 Spark 是主要引擎,或 Unity Catalog 已經是治理層時選擇。與 Spark 的整合最強;其他引擎可以讀取,但寫入支援各異。
- Apache Iceberg:當多個引擎必須寫入,或者您想要與廠商無關的目錄(REST)時選擇。v3 增加了刪除向量、列血緣和變體類型;在升級資料表之前,請確認路徑上的每個引擎和目錄都支援您要使用的 v3 功能,因為舊的讀取器在較新的格式版本上會失敗。
- DuckLake:針對小型和中型資料、單一團隊,以及您已作為目錄操作的 SQL 資料庫(元資料存在於純文字資料表中)時選擇。執行成本低,生態系年輕;當 Spark、Trino 或 Snowflake 必須寫入相同資料表時,請不要選擇它。
- 互通性層(Delta UniForm、用於 Delta 資料表的 Iceberg 中繼資料)是讀取路徑。每個資料表選擇一種格式作為記錄寫入器。
- 之後更改格式需要重寫或進行元資料轉換;請謹慎選擇記錄寫入器。
4. 目錄
- 目錄是提交協調器和存取控制點;在格式細節變得重要之前先選擇它。
- Unity Catalog:以 Databricks 為中心的預設選項;為 Delta 和 Iceberg 提供治理。
- AWS Glue 和 S3 Tables:當技術堆疊是 AWS 原生時選擇;S3 Tables 會為您管理壓縮,但會將您綁定在 AWS 上。
- Iceberg REST 目錄(例如 Polaris):為了多引擎、多雲端中立性而選擇。
- DuckLake:目錄是一個 SQL 資料庫;使用 Postgres 作為後端以支援多使用者使用,SQLite 或 DuckDB 僅適用於單一寫入器。
- 永遠不要讓兩個目錄註冊相同的資料表路徑;對其中一個目錄的提交會破壞另一個目錄的檢視。
5. 維護(安排它,否則資料表會退化)
- 壓縮小型檔案:Delta 的
OPTIMIZE、Iceberg 的rewrite_data_files、DuckLake 的合併相鄰檔案。在大量資料攝入後執行,而不是每次寫入都執行。 - 移除舊資料:Delta 的
VACUUM、Iceberg 的expire_snapshots加上孤立檔案清理、DuckLake 的快照過期後清理。保留時間必須超過您最長的讀取者或時間旅行視窗,因為在執行中的讀取器下刪除檔案會導致其出錯。 - 叢集或 Z-order 僅針對出現在篩選條件中的欄位進行;重新叢集會重寫資料並消耗計算資源。
- 在之前和之後檢查檔案數量和平均檔案大小;如果維護作業沒有改變任何一個,則代表它沒有做任何事。
- 如果引擎提供,請先試用乾燥模式的 VACUUM 或孤立檔案移除。
6. MERGE、Schema 演進、時間旅行
- MERGE:先在比對鍵上對來源進行去重(重複的來源比對會失敗或導致雙重套用)。在
ON子句中包含一個分區或叢集謂詞,以便引擎修剪檔案而不是掃描整個資料表。 - 在大型資料表上,優先使用附加(append)加上定期 MERGE,或使用冪等鍵,而不是重複執行全表 MERGE。
- Schema 演進:新增欄位是安全的;重新命名和型別擴展取決於格式和版本(Delta 中的欄位對應,Iceberg 中的欄位 ID)。在重新命名之前,請檢查相關讀取器是否支援。永遠不要在不受控的生產管線輸入上啟用自動 schema 合併。
- 時間旅行是除錯、稽核和回滾的工具,而不是備份。它會消耗保留的檔案;保留時間越長,儲存空間越多,元資料也越慢。請謹慎設定保留時間,並為關鍵資料表保留真實的備份。
7. PySpark 4.x 要點
- 轉換是懶執行的;在採取行動之前什麼都不會執行。建立一個計畫,觸發一次寫入;重複的
count()或show()會重新執行整個計畫,除非您刻意使用了快取。 - 避免對大型資料使用
collect()、toPandas()和.rdd;它們會將所有資料拉取到驅動程式。請先進行限制或彙總。 - 使用內建函數或 SQL 表達式,而不是 Python UDF。UDF 會阻礙最佳化器的下推並序列化資料列。如果無法避免,請使用向量化(Arrow)UDF。
- 寬操作(join、groupBy、window、distinct)會進行混洗。在它們之前進行過濾和投影;使用
explain()檢查計畫。 - 當一個小型關聯表可以放入執行器記憶體時,廣播它(
broadcast(df));不要廣播一個無界的資料表。 - Adaptive Query Execution 在 Spark 4.x 中預設開啟,並處理分區合併、傾斜連接和連接策略切換;在閱讀計畫後進行調整,而不是之前。
- 傾斜表現為一個緩慢的任務;只有在 AQE 證明不足之後,才對熱鍵進行加鹽或隔離處理。
- Spark Connect 是客戶端-伺服器模式:程式碼作為輕量級客戶端通過遠端工作階段執行。不要在 Connect 程式碼中依賴驅動程式本地的 JVM 內部(
sparkContext、RDD API)。 - 透過壓縮來指定明確的目標檔案大小進行寫入,而不是重新分區到一個猜測的數字。
8. 本機優先路徑
在將程式碼部署到叢集之前,使用 DuckDB 或 polars 進行開發和測試。
- DuckDB 透過擴充功能讀取 Delta 和 Iceberg,並附加 DuckLake 目錄;polars 使用謂詞下推方式惰性讀取 Delta 和 Iceberg。
- 使用樣本或一個分區對照實際的資料表配置進行測試,以便檔案修剪行為相符。
- 在實際引擎上確認寫入語意(MERGE、刪除向量、v3 功能);本機讀取器可能落後於格式功能。將成功的本機讀取視為必要條件,而非充分條件。
程序
- 取得數字:大小、成長量、寫入器、讀取器、引擎。 2. 逐步檢查第 1 節;停在最便宜且符合需求的方案。 3. 如果是湖倉:選擇目錄,然後選擇格式(第 4 節、第 3 節)。 4. 在首次載入之前設定配置和維護計畫。 5. 在本機建立原型(第 8 節),然後在叢集上執行並審查
explain()的輸出。 6. 使用檔案計數和查詢計畫進行驗證,而不是憑假設。
已知陷阱和修正:請參閱 references/gotchas.md。