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 似乎只是 Cloudberry 众多优化中的一个微小特性;但从内部架构看,它是连接查询优化器、执行器和存储引擎之间的一条重要链路。
它让 Cloudberry 能够:
无感知地提升用户查询体验; 无需用户编写复杂 SQL 或手动调优参数; 在执行链路中高效落地实时过滤的同时,实现 OLAP 场景下分布式查询的持续加速。
这也是为什么我们在 Cloudberry 中坚定地选择将 Runtime Filter 作为系统性能力而非临时补丁去实现。
如果你是 Cloudberry 的用户,当你在执行复杂 Join 查询时,可能会注意到执行计划中的一行提示:
Rows Removed by Pushdown Runtime Filter: 783,421
这就是 Runtime Filter 在默默地替你工作,为你节省了将近 78 万行无效数据的处理、传输和计算。 而这,正是 Cloudberry 实现 Runtime Filter 的真正意义。
下一篇,我们将更具体地介绍:Cloudberry Runtime Filter 是如何工作的?带你看见 Runtime Filter 在执行时如何被构建、如何被下推、如何真正“动起来”,以及我们在实现过程中遇到的具体技术挑战和解决方案。
👇🏻️扫码加入 Apache Cloudberry 交流群👇🏻️





