Apache Cloudberry™ (Incubating) 是 Apache 軟件基金會孵化項目,由 Greenplum 和 PostgreSQL 衍生而來,作為領先的開源 MPP 數據庫,可用於建設企業級數據倉庫,並適用於大規模分析和 AI/ML 工作負載。
GitHub: https://github.com/apache/cloudberry
文章作者:張玥,酷克數據研發工程師;整理:酷克數據
在優化分佈式數據庫查詢性能時,有一個長期被開發者忽視卻真實存在的成本:在 Join 前沒有及時過濾無效數據,導致 CPU、內存和網絡被浪費在處理這些永遠不可能匹配的行上。
在 Apache Cloudberry 的用户社區中,我們經常遇到這樣的提問:“同樣的數據量,為什麼這個 Join 查詢在我們的集羣上跑得並不快?” 當我們排查執行計劃時,往往會發現問題的根源在於 Join 的 probe 端(大表側)始終在無差別掃描所有行,即使這些行根本不可能匹配到小表上的 Join Key。
這是很多數據庫系統都曾經歷的 “成長煩惱”,也是為什麼我們在 Cloudberry 中堅定地實現並落地 Runtime Filter(動態過濾器)。
為什麼傳統 Hash Join 在大表上會成為性能瓶頸?
傳統 Hash Join 的執行流程非常簡單直接:先在小表上構建哈希表,然後掃描大表,對每一行都做一次哈希匹配。對用户來説,這種實現是 “透明” 的,因為它在任何場景下都能正確返回結果,但問題在於,當大表體量非常大時,這種 “掃描一切再判斷” 的策略就成了資源黑洞。
具體來説:
CPU 被無效使用。 大表掃描過程中,每一行都要經過哈希探測,即便它們不可能匹配,也在消耗 CPU。
內存負載高。 無效行被加載、緩存、參與後續算子處理,擠佔了真正有效數據的內存空間。
網絡帶寬浪費。 在分佈式執行時,大表中這些無效行可能被傳輸到其他節點參與分佈式 Join,白白浪費帶寬。
如果 Join 能在真正開始之前就知道哪些行必然不會命中,那麼這些 CPU、內存和網絡資源完全可以節省下來,用於處理真正有價值的數據。
這就是 Runtime Filter 存在的意義。
所謂 Runtime Filter,本質上是 在查詢執行時根據小表 Join Key 動態生成的過濾器,將其 “提前” 下推到大表掃描節點,對大表行做快速預過濾,讓那些不可能匹配的行直接在掃描時就被丟棄。
它並不複雜:
在小表構建哈希表時,同時根據 Join Key 創建 Bloom Filter(或 Range Filter)。
將這個過濾器下推到大表掃描(SeqScan)階段。
在大表掃描時,Join Key 會先經過過濾器檢查,如果不可能命中,直接丟棄。
結果是,大表參與 Join 的行數鋭減,執行時間隨之下降,用户感覺就是:“查詢快了不少。”
值得一提的是,Runtime Filter 並不是 Cloudberry 獨有的優化,Spark SQL、Trino(Presto)、Apache Doris 等主流系統都早已在生產環境使用這一技術。
在 TPC-H、TPC-DS 等標準測試中,Runtime Filter 可以幫助部分 Join-heavy 的查詢實現 2-10 倍的加速,且這些加速並不依賴於複雜調優參數,而是來源於最樸素的道理:“能不處理的行就不要處理。”
Cloudberry 是如何實現 Runtime Filter 的?
在實現 Runtime Filter 的過程中,我們遵循了 Cloudberry 的整體理念:簡潔、高效、易擴展。
首先,我們使用 Bloom Filter 作為主要過濾器類型。原因很簡單:Bloom Filter 是一種概率型過濾器,佔用空間極小(通常幾個 MB),通過多個哈希函數判斷某個值是否可能存在,即便存在假陽性(放行無效行),也不會出現假陰性(誤過濾正確行),這保證了最終結果的一致性。
其次,對於數值型 Join Key(如時間戳、整型 ID 等),我們也支持 Range Filter。它只記錄 Join Key 的最小值和最大值,用於直接排除範圍外的數據,更加簡單高效。
我們在實現層面選擇了 LOCAL 模式(進程內下推):
Bloom Filter 和 Range Filter 的構建與下推都在同一進程內完成,無需跨進程或跨節點通信。
下推到大表 SeqScan 節點後,過濾器直接作用於掃描過程,幾乎沒有額外延遲。
這種模式實現簡單,效果立竿見影,避免了引入不必要的分佈式複雜性,同時帶來顯著的執行性能提升。
實際效果怎麼樣?
在 Cloudberry 的性能基準測試中,我們使用了 TPC-DS 10GB 和 100GB 數據集進行了對比:
在 10GB 測試集上,開啓 Runtime Filter 後,查詢總耗時從 939 秒降低到 779 秒,縮短了約 17%。
在 100GB 測試集上,從 5270 秒降低到 4365 秒,提升同樣在 17% 左右。
需要注意的是,這種性能提升並非源於魔法,而是因為 Runtime Filter 在 Join 前就過濾掉了大量無用數據,使得 Join 的輸入更 “乾淨”,從而減少了計算、內存和網絡負擔。
在實際用户環境中,這種加速效果往往更明顯,特別是在 Join Key 基數較小、過濾效果明顯的場景中,Runtime Filter 能讓長時間跑不完的分析報表大幅縮短執行時間。
Runtime Filter 實現
在執行 Hash Join 構建哈希表時,Cloudberry 會在內部同步生成 Bloom Filter 或 Range Filter:
Bloom Filter 通過哈希函數將小表的 Join Key 值映射到位數組,實現快速的概率過濾。內存消耗極小(通常僅需幾 MB),但可能存在假陽性。
Range Filter 則記錄 Join Key 的最小值和最大值,對於數值範圍連續的數據(如時間戳、整型 ID)過濾效果更好。
這些過濾器在小表掃描時被無感知地構建,完全不需要額外掃描,也不需要二次計算,真正做到 “順手” 完成。
下推至大表掃描節點
過濾器構建完成後,最關鍵的步驟是將它下推到大表的 SeqScan 節點,讓過濾器在掃描時生效。
在 Cloudberry 中,Join 構建和大表掃描通常位於同一執行進程內,因此過濾器可以以內存指針的方式直接傳遞給大表掃描節點,避免了序列化和網絡通信的額外成本。
在大表執行掃描時,每當拉取下一行數據時,系統會先將該行數據的 Join Key 列送入過濾器檢查:
如果不在 Range Filter 範圍內,直接丟棄。
如果 Bloom Filter 判斷 “不存在”,直接丟棄。
只有通過過濾的行,才會繼續進入 Hash Join 參與探測。
這種在掃描時 “預過濾” 的模式,與 Cloudberry 的執行流水線完美適配,不會破壞流水線調度,也不會引入額外鎖和同步延遲。
LOCAL 模式下推
業界的一些引擎會選擇在跨節點環境中通過 GLOBAL 模式下推過濾器,將過濾器同步到所有數據節點,實現更大範圍的預過濾。
在 Cloudberry 的第一階段,我們刻意選擇了 LOCAL 模式(進程內下推):
因為大部分 Broadcast Join 的場景,過濾器在進程內就足夠高效;
避免了跨節點網絡傳輸和序列化帶來的延遲;
讓過濾器的構建和應用零延遲生效,讓收益最大化且穩定。
這種實現方式使 Runtime Filter 成為了 Cloudberry 查詢鏈路中 “真正無感知但持續生效” 的能力。
在執行計劃中可觀測,讓加速 “看得見”
Runtime Filter 不僅僅是默默執行的幕後加速器,它在執行計劃中是可被用户清晰感知的。當用户執行 EXPLAIN ANALYZE 時,可以看到類似如下輸出:
Rows Removed by Pushdown Runtime Filter: 4,328,191
意味着有 430 萬行在掃描時就被 Runtime Filter 丟棄了,不再進入 Hash Join 的計算管道。
這種 “可見可觀測” 的設計對 DBA、性能調優工程師非常友好:
便於判斷 Runtime Filter 是否生效;
能驗證過濾效果是否達到預期;
為後續優化 SQL 提供直觀依據。
代碼中的 “真實細節”
在 Cloudberry 的執行器中,Runtime Filter 並非獨立流程,而是通過核心結構 AttrFilter 與 Hash Join 和 SeqScan 深度集成。
AttrFilter 在執行時記錄:
Join 鍵範圍(min/max)用於 Range Filter;
Bloom Filter 實例用於概率過濾;
Join 鍵位置映射(rattno/lattno)確保列正確匹配;
關聯到目標 SeqScan 節點的 PlanState 指針,用於精確下推。
構建過程完全與 Hash Join 的 MultiExecPrivateHash 流程同步:
在小表哈希表構建時調用 AddTupleValuesIntoRF 將值寫入 Bloom Filter 或更新範圍;
構建完成後調用 PushdownRuntimeFilter 下推過濾器到目標掃描節點;
在查詢結束時自動調用 FreeRuntimeFilter 回收內存,保證系統穩定性和內存安全。
這種嵌入式實現方式,使 Runtime Filter 成為了 Cloudberry 查詢執行過程中 “天然存在” 的優化能力。
結語
在 Cloudberry,我們希望大部分優化能力都能做到 “對用户無感,對系統有益”,Runtime Filter 正是這樣一種能力。
它不需要用户額外學習參數,不需要寫複雜 SQL Hint,也不需要在執行前進行特別配置,但只要你的查詢包含 Join,它就會自動工作,為你節省時間與資源。
Runtime Filter 的使命非常純粹: 在 Join 前,讓不可能命中的行在最便宜的階段被提前過濾掉,讓資源只用於真正有價值的計算。
未來,我們將繼續擴展 Runtime Filter:
在合適的場景中引入 GLOBAL 模式支持,跨節點做全局預過濾;
支持 IndexScan / BitmapScan 下推;
提供更加智能的過濾器精度控制;
實現與自適應並行度、管道執行更深度融合。
但無論演化到何種程度,這項能力的本質始終不變: 用最簡單的方法,讓 Cloudberry 更快、更穩、更省。
如果你想了解更多 Cloudberry 在執行鏈路中的底層優化實踐,歡迎繼續關注,我們會持續發佈更多底層設計與優化實戰分享。