AggregateExpandDistinctAggregatesRule是 Calcite 中一条非常核心的聚合展开规则,它主要处理COUNT(DISTINCT x)、SUM(DISTINCT x)这类带DISTINCT的聚合表达式。由于DISTINCT聚合往往不能直接下推到统一的物理实现中,Calcite 会在优化阶段把它改写成更基础的Aggregate、Project、Join或GROUPING SETS组合。本文结合源码实现,分析这条规则的匹配条件、几条主要改写路径、GROUPING SETS场景下的特殊处理,以及它与AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN变体之间的关系。1. 规则要解决什么问题带DISTINCT的聚合,本质上是在“先去重,再聚合”。例如:SELECTdeptno,COUNT(DISTINCTename)FROMempGROUPBYdeptno;从语义上看,它不是简单的:GROUPBYdeptno之后直接做一次COUNT就能完成的,因为COUNT看到的输入需要先在每个分组内按ename去重。再比如:SELECTdeptno,COUNT(DISTINCTename),SUM(sal)FROMempGROUPBYdeptno;这里问题更复杂:COUNT(DISTINCT ename)需要“分组内去重”;SUM(sal)不需要去重;两种聚合的输入粒度并不一致。这就是AggregateExpandDistinctAggregatesRule的存在价值。它要把这种“语义上复杂、执行上不统一”的聚合,改写成优化器和执行器更容易处理的 RelNode 组合。对应源码位置:core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java规则类注释已经把核心思想说得很直接:如果所有DISTINCT聚合都作用在同一组参数上,一个额外的Aggregate往往就够了;如果有多组不同参数的DISTINCT聚合,就需要拆成多个Aggregate,再通过Join或GROUPING SETS组合回来。2. 规则入口很简单,但前置判断很多这条规则的匹配模式本身很简单,只匹配一个LogicalAggregate:b.operand(LogicalAggregate.class).anyInputs()也就是说,它不是像很多规则那样要求固定的子树形状,而是只要看到Aggregate就先接管,再在onMatch里判断能不能改、该怎么改。onMatch一进来先做两个硬判断:当前Aggregate必须真的包含DISTINCT聚合;如果当前配置不用GROUPING SETS,那遇到多groupSets的聚合要直接退出。对应代码:if(!aggregate.containsDistinctCall()){return;}if(!config.isUsingGroupingSets()aggregate.groupSets.size()1){return;}第二条很关键。因为这个规则有两个公开变体:CoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATESCoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN前者默认isUsingGroupingSets() == true,更偏向使用GROUPING SETS改写;后者显式关闭GROUPING SETS,在复杂 distinct 场景下改用JOIN方式展开。3. onMatch 的第一步:先把聚合调用分类规则一开始会把所有聚合调用拆成几类:distinctAggCalls:带DISTINCT的聚合;nonDistinctAggCalls:不带DISTINCT的聚合;filterCount:带FILTER的聚合个数;distinctCallArgLists:按“参数列表 + filterArg”对 distinct 聚合分组。这里最值得注意的是这句:Pair.of(aggCall.getArgList(),aggCall.filterArg)也就是说,Calcite 认为下面两种聚合不是同一种 distinct 模式:C
【Calcite 系列】深入理解 Calcite 的 AggregateExpandDistinctAggregatesRule
AggregateExpandDistinctAggregatesRule是 Calcite 中一条非常核心的聚合展开规则,它主要处理COUNT(DISTINCT x)、SUM(DISTINCT x)这类带DISTINCT的聚合表达式。由于DISTINCT聚合往往不能直接下推到统一的物理实现中,Calcite 会在优化阶段把它改写成更基础的Aggregate、Project、Join或GROUPING SETS组合。本文结合源码实现,分析这条规则的匹配条件、几条主要改写路径、GROUPING SETS场景下的特殊处理,以及它与AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN变体之间的关系。1. 规则要解决什么问题带DISTINCT的聚合,本质上是在“先去重,再聚合”。例如:SELECTdeptno,COUNT(DISTINCTename)FROMempGROUPBYdeptno;从语义上看,它不是简单的:GROUPBYdeptno之后直接做一次COUNT就能完成的,因为COUNT看到的输入需要先在每个分组内按ename去重。再比如:SELECTdeptno,COUNT(DISTINCTename),SUM(sal)FROMempGROUPBYdeptno;这里问题更复杂:COUNT(DISTINCT ename)需要“分组内去重”;SUM(sal)不需要去重;两种聚合的输入粒度并不一致。这就是AggregateExpandDistinctAggregatesRule的存在价值。它要把这种“语义上复杂、执行上不统一”的聚合,改写成优化器和执行器更容易处理的 RelNode 组合。对应源码位置:core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java规则类注释已经把核心思想说得很直接:如果所有DISTINCT聚合都作用在同一组参数上,一个额外的Aggregate往往就够了;如果有多组不同参数的DISTINCT聚合,就需要拆成多个Aggregate,再通过Join或GROUPING SETS组合回来。2. 规则入口很简单,但前置判断很多这条规则的匹配模式本身很简单,只匹配一个LogicalAggregate:b.operand(LogicalAggregate.class).anyInputs()也就是说,它不是像很多规则那样要求固定的子树形状,而是只要看到Aggregate就先接管,再在onMatch里判断能不能改、该怎么改。onMatch一进来先做两个硬判断:当前Aggregate必须真的包含DISTINCT聚合;如果当前配置不用GROUPING SETS,那遇到多groupSets的聚合要直接退出。对应代码:if(!aggregate.containsDistinctCall()){return;}if(!config.isUsingGroupingSets()aggregate.groupSets.size()1){return;}第二条很关键。因为这个规则有两个公开变体:CoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATESCoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN前者默认isUsingGroupingSets() == true,更偏向使用GROUPING SETS改写;后者显式关闭GROUPING SETS,在复杂 distinct 场景下改用JOIN方式展开。3. onMatch 的第一步:先把聚合调用分类规则一开始会把所有聚合调用拆成几类:distinctAggCalls:带DISTINCT的聚合;nonDistinctAggCalls:不带DISTINCT的聚合;filterCount:带FILTER的聚合个数;distinctCallArgLists:按“参数列表 + filterArg”对 distinct 聚合分组。这里最值得注意的是这句:Pair.of(aggCall.getArgList(),aggCall.filterArg)也就是说,Calcite 认为下面两种聚合不是同一种 distinct 模式:C