【Calcite 系列】深入理解 Calcite 的 AggregateRemoveRule
AggregateRemoveRule用来删掉“其实已经没有必要存在”的Aggregate。它成立的前提是:输入在当前 group key 上已经唯一,而且所有聚合调用都可以被化成单行表达式或常量。本文结合源码实现,分析这条规则为什么能把SUM(x)退化成x,为什么对SUM0特别保守,以及它是如何在删除聚合后补出对应Project的。
1. 规则要解决什么问题
如果输入已经在 group key 上唯一,例如:
SELECTempno,SUM(sal)FROMempGROUPBYempno,deptno;且empno本身就是唯一键,那么每个组实际上只有一行。此时:
SUM(sal)和直接取该行的sal是等价的,整个Aggregate就可能被移除。
2. 触发条件
规则先用areColumnsUnique(input, aggregate.getGroupSet())判断输入在 group key 上是否唯一。
如果不唯一,直接退出。
然后还要求每个AggregateCall都满足canFlatten(...),也就是:
- 没有
FILTER; - 聚合函数是
SqlSingletonAggFunction或SqlStaticAggFunction。
3. singleton 聚合和 static 聚合的区别
3.1 singleton
像SUM(x)、MIN(x)、MAX(x)这类,在“每组最多一行”时可以退化成单行表达式。
源码会通过:
splitter.singleton(...)得到对应表达式。
3.2 static
某些聚合在特定 group set 下甚至可以直接化成常量,例如GROUPING(...)一类静态结果。
源码通过:
staticAggFunction.constant(...)直接生成常量表达式。
4. 为什么遇到 SUM0 要直接退出
源码里专门有:
if(aggregation.getKind()==SqlKind.SUM0){return;}注释说明是为了避免潜在的无限规则匹配。因为SUM0常常来自其他规则把SUM规约后的中间形态,如果这里继续激进消掉,可能和其他规则来回震荡。
5. 删除 Aggregate 后怎么保持输出不变
规则删掉Aggregate后并不会直接返回原输入,而是:
- 先把新输入压入
RelBuilder; - 构造投影列表:
- group key 列保留;
- 每个聚合调用变成对应表达式或常量;
- 如果原 aggregate 只有 group key 没有聚合函数,而输入列更多,还要补一层
Project保持列裁剪效果。
6. 为什么这条规则依赖元数据
它成立的关键不在语法,而在:
mq.areColumnsUnique(...)如果元数据不能证明输入唯一,哪怕用户肉眼觉得“这看起来就只有一行”,规则也不会触发。
这说明它是一条高度依赖唯一性推断的替换规则。
7. 代表性测试
RelOptRulesTest里 8010 行附近有一整组测试:
testAggregateRemove1到testAggregateRemove7
覆盖了:
- 输入唯一时应删除;
- 无聚合函数时应删除;
COUNT(mgr)因可空列需要正确转表达式;AVG这类不能删;- 非 simple aggregate 不能删;
- group key 不唯一不能删。
总结
AggregateRemoveRule的核心逻辑很明确:
- 输入已经在 group key 上唯一;
- 聚合函数可以退化成单行表达式或静态常量;
- 删除
Aggregate后再用Project保持输出 schema。
它是一条典型的“元数据驱动消除规则”,真正难的不是删节点,而是先证明“删掉以后语义完全不变”。
