一. 前言
Presto的局部聚合是提升聚合计算效率的一个重要优化手段。其原理就是对于count,sum等聚合算子先在每一个split中完成局部聚合后再exchange到coordinator中完成最后的聚合计算。通过局部聚合计算可以大大减少聚合操作中数据的传输量从而提升最后的计算效率。
二. 局部聚合
局部聚合是指计划执行树中的Aggregate(PARTIAL) 节点,如下图为其的一个样例:
在Presto中,局部聚合算子的生成是通过PushPartialAggregationThroughExchange的优化器来实现,其实现思路是将一个最终聚合Aggregate(FINAL)经过PushPartialAggregationThroughExchange优化器的时候,附加一个 局部聚合算子Aggregate(PARTIAL),并且将附件的 Aggregate(PARTIAL)下推到Exchange算子底下实现的。其实现代码的走读如下:
private PlanNode split(AggregationNode node, Context context)
{
.....
entry是sql中的所有聚合操作算子,比如cout(a), max(b)等
for (Map.Entry<Symbol, AggregationNode.Aggregation> entry : node.getAggregations().entrySet()) {
// 地下主要获取到聚合操作的列和聚合算子
AggregationNode.Aggregation originalAggregation = entry.getValue();
String functionName = metadata.getFunctionAndTypeManager().getFunctionMetadata(originalAggregation.getFunctionHandle()).getName().getObjectName();
FunctionHandle functionHandle = originalAggregation.getFunctionHandle();
InternalAggregationFunction function = metadata.getFunctionAndTypeManager().getAggregateFunctionImplementation(functionHandle);
Symbol intermediateSymbol = context.getSymbolAllocator().newSymbol(functionName, function.getIntermediateType());
// order by做本地聚合会导致数据错乱,因为order by的操作的场景不进行优化,全部数据exchange到coordaintor处理
checkState(!originalAggregation.getOrderingScheme().isPresent(), "Aggregate with ORDER BY does not support partial aggregation");
// intermediateAggregation 就是partial Aggregation
intermediateAggregation.put(
intermediateSymbol,
new AggregationNode.Aggregation(
new CallExpression(
functionName,
functionHandle,
function.getIntermediateType(),
originalAggregation.getArguments(),
Optional.empty()),
originalAggregation.getArguments(),
originalAggregation.isDistinct(),
originalAggregation.getFilter(),
originalAggregation.getOrderingScheme(),
originalAggregation.getMask()));
// rewrite final aggregation in terms of intermediate function
finalAggregation.put(
entry.getKey(),
new AggregationNode.Aggregation(
new CallExpression(
functionName,
functionHandle,
function.getFinalType(),
ImmutableList.<RowExpression>builder()
.add(new VariableReferenceExpression(intermediateSymbol.getName(), function.getIntermediateType()))
.addAll(originalAggregation.getArguments()
.stream()
.filter(PushPartialAggregationThroughExchange::isLambda)
.collect(toImmutableList()))
.build(),
Optional.empty()),
ImmutableList.<RowExpression>builder()
.add(new VariableReferenceExpression(intermediateSymbol.getName(), function.getIntermediateType()))
.addAll(originalAggregation.getArguments().stream()
.filter(PushPartialAggregationThroughExchange::isLambda)
.collect(toImmutableList()))
.build(),
false,
Optional.empty(),
Optional.empty(),
Optional.empty()));
}
// 生成本地聚合算子
PlanNode partial = new AggregationNode(...intermediateAggregation, PARTIAL...);
// 生成 Aggregate(FINAL) -- Aggregate(PARTIAL)的计划树
return new AggregationNode(...partial....finalAggregation...);
}
经过上边的split操作后,还需要通过PushPartialAggregationThroughExchange::pushPartial操作将PARTIAL Aggregate下推到exchange的地下完成整个计划执行树的聚合聚合优化。
三. 中间聚合
在Presto中,还有一个中间聚合Aggregate(INTERMEDIATE)的聚合态,其优化目的是为了Final 聚合在送给coordaintor之前,再让worker做一次局部聚合的再聚合操作,从而减轻coordinator的计算压力和提升聚合计算的并行度。如下是一个中间聚合的计划执行树样例:
中间聚合优化在AddIntermediateAggregations优化规则中实现的。其实现原理是从计划执行树的上往下找到Final aggregation节点中的PARTIAL AggregationNode的Source节点,然后在PARTIAL AggregationNode Source中增加一个ExchangeNode 和INTERMEDIATE AggregationNode来完成计划执行树的优化。
中间聚合默认是不开启的,需要通过enable_intermediate_aggregations将其开启。