在向量化執行系統中,表達式構建是不可或缺的基礎環節。無論是 SQL 中的投影、篩選,還是分區、聚合、排序,最終都需轉化為底層執行引擎能識別和執行的表達式樹。而在 Apache Cloudberry 向量化執行框架中,這一過程由 Gandiva 表達式引擎負責完成。
隨著數據規模與查詢復雜度的提升,我們逐漸意識到,表達式構建本身正成為影響執行性能的關鍵路徑之一。特別是在高并發、多表達式拼接的場景下,構建過程的性能瓶頸愈加突出。本文將結合實際優化案例,分享我們如何識別問題、設計優化方案,并用火焰圖驗證成效。
為何選 Gandiva?JIT + Arrow 的組合拳
Gandiva 是 Apache Arrow 項目中的子模塊,它基于 LLVM 構建 JIT 編譯能力,專為高性能、批量化的列式計算而設計。我們選擇 Gandiva 作為表達式引擎的主要原因有三點:
- 向量化執行友好:Gandiva 表達式以 Arrow RecordBatch 為輸入/輸出單位,與 Cloudberry 的內存格式完全兼容,避免額外序列化/反序列化開銷。
- JIT 編譯能力強:Gandiva 支持將表達式編譯為本地機器碼,執行效率顯著優于解釋執行。
- 表達式樹抽象清晰:其表達式結構基于語法樹(AST),便于分析、合并、轉換、優化。
但“強大”背后也隱藏著一個問題:表達式構建過程并非“零成本”,尤其在表達式數量和深度快速增長時,構建開銷成為了不容忽視的負擔。
原始構建路徑的問題:節點重復 & 樹結構過深
在未優化前,我們采用“逐表達式構建”的方式——每處理一條 SQL 表達式,就從頭創建一棵新的表達式樹。這種策略在簡單查詢下運行良好,但在復雜嵌套查詢、窗口函數、聯表計算等場景下暴露出以下問題:
- 公共子表達式重復構建:同一表達式片段(如 lower(colA))在不同上下文中多次出現時,每次都重新生成節點,造成冗余。
- 表達式樹結構深且復雜:表達式鏈條變長時,嵌套層級加深,構建耗時近似呈線性增長。
- Hash 邏輯不穩定:相同表達式結構,由于構建路徑差異導致節點 hash 不一致,影響緩存和優化判斷。
我們對典型查詢的表達式構建過程進行了耗時統計,結果顯示:
- 在包含 20+ 表達式的復雜查詢中,表達式構建耗時占整體查詢時間的 10%~15%;
- 其中約 40% 的表達式為可復用的子表達式,但未被有效識別與復用;
- 構建階段的所有開銷幾乎全部集中在 on-CPU 路徑上,火焰圖顯示 CreateExpressionNode、ToArrowNode 等函數在 CPU 調用棧中占比極高,成為構建瓶頸的主要耗時點。
這些現象表明:表達式構建過程不僅費時,而且浪費資源。
優化策略:公共子表達式識別 + 哈希原子化
我們采用兩項優化手段來重構表達式構建路徑:
- 公共子表達式識別(CSE)
引入表達式 DAG 結構,在構建過程中為每個子表達式生成唯一 key(基于語義簽名),并放入全局表達式緩存池。后續若再次請求相同表達式,直接復用已有子樹。
- 優點:減少冗余節點構建,降低構建深度;
- 技術點:等價表達式歸一化(如 a + b vs b + a)、表達式 hash 去重。
- 哈希表達式原子化
將每一個表達式節點封裝為具有確定性 hash 的原子單元,避免因構建路徑差異導致 hash 沖突。統一采用 結構 hash + 類型信息 + 參數簽名 的組合哈希策略,確保緩存命中率提升。
優化后,我們實現了表達式構建路徑的“結構性去重”:從構建“樹”轉為拼裝“塊”,如搭積木般復用構建單元,降低系統負擔。
優化效果對比:結構簡化 & 構建耗時下降
通過對比優化前后在復雜 SQL 下的表達式構建過程,我們觀察到以下顯著變化:
不僅節點數量明顯下降,構建時間也隨之降低了50%以上,特別是在復合查詢中表現尤為明顯。
火焰圖驗證:構建路徑 on-CPU 時間顯著下降
我們進一步通過 perf 工具配合火焰圖對比優化前后的 CPU 使用情況,焦點集中在表達式構建階段。
優化前的火焰圖中,Gandiva::TreeExprBuilder::MakeExpression() 及其內部調用占據主火焰圖的 30% 高度,顯著吞噬 on?CPU 資源。
優化后,火焰圖中該函數堆棧深度顯著縮減,僅占主圖不到 10%,并可見更多時間釋放給后續執行邏輯,如 Eval、Filter、Project 等。
這說明:表達式構建從 CPU 消耗的“主角”,退回到了其應有的“配角”角色。
表達式構建常被認為是“編譯期行為”,但在現代向量化系統中,它的性能表現直接影響執行鏈路的起跑速度。
通過本次優化,我們驗證了如下幾點:
- 表達式構建本身具有顯著的優化空間;
- 結構性去重 比單純加快構建速度更有效;
- 可觀測性工具(如火焰圖) 是評估優化效果的關鍵利器。
這也為后續優化其他執行環節(如重分布、調度、緩存)提供了經驗模板:先觀測,再定位,再結構重構。