MergeSetOperations.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.nereids.rules.rewrite;
import org.apache.doris.nereids.rules.Rule;
import org.apache.doris.nereids.rules.RuleType;
import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation;
import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Merge nodes of the same type and same qualifier.
* <p>
* eg: select k1, k2 from t1 union select 1, 2 union select d1, d2 from t2;
* <pre>
* union
* / \
* union scan3
* / \
* scan1 scan2
* -->
* union
* / | \
* scan1 scan2 scan3
* </pre>
* Notice: this rule ignore Except.
* Relational Algebra: Union (R U S), Intersect Syntax: (R ∩ S), Except Syntax: (R - S)
* TODO: Except need other Rewrite.
* <ul> (R - S) U T = (R U T) - S </ul>
* <ul> (R - S) U (T - U) = (R U T) - (S U U) </ul>
* <ul> R - (S U T) = (R - S) - T </ul>
* <ul> R - (S - T) = (R - S) U T </ul>
* <ul> ...... and so on </ul>
*/
public class MergeSetOperations implements RewriteRuleFactory {
@Override
public List<Rule> buildRules() {
return ImmutableList.of(
RuleType.MERGE_SET_OPERATION.build(
logicalIntersect(any(), any())
.when(MergeSetOperations::canMerge)
.then(MergeSetOperations::mergeIntersect)
),
RuleType.MERGE_SET_OPERATION.build(
logicalUnion().then(MergeSetOperations::mergeUnion)
)
);
}
private static Plan mergeUnion(LogicalUnion parentUnion) {
ImmutableList.Builder<Plan> newChildren = ImmutableList.builder();
ImmutableList.Builder<List<SlotReference>> newChildrenOutputs = ImmutableList.builder();
ImmutableList.Builder<List<NamedExpression>> constantList = ImmutableList.builder();
boolean needMerge = false;
for (int i = 0; i < parentUnion.arity(); i++) {
Plan child = parentUnion.child(i);
if (canMerge(parentUnion, child) && child.getOutput().size() == parentUnion.getOutput().size()) {
LogicalUnion childUnion = (LogicalUnion) child;
newChildren.addAll(childUnion.children());
List<Integer> outputOrder = getChildOutputOrder(parentUnion, i, childUnion);
for (List<SlotReference> outputs : childUnion.getRegularChildrenOutputs()) {
newChildrenOutputs.add(reorderOutputs(outputs, outputOrder));
}
for (List<NamedExpression> constants : childUnion.getConstantExprsList()) {
constantList.add(reorderOutputs(constants, outputOrder));
}
needMerge = true;
} else {
newChildren.add(child);
newChildrenOutputs.add(parentUnion.getRegularChildOutput(i));
}
}
if (!needMerge) {
return parentUnion;
}
constantList.addAll(parentUnion.getConstantExprsList());
return parentUnion.withChildrenAndConstExprsList(
newChildren.build(), newChildrenOutputs.build(), constantList.build()
);
}
private static Plan mergeIntersect(LogicalIntersect parentSetIntersect) {
ImmutableList.Builder<Plan> newChildren = ImmutableList.builder();
ImmutableList.Builder<List<SlotReference>> newChildrenOutputs = ImmutableList.builder();
for (int i = 0; i < parentSetIntersect.arity(); i++) {
Plan child = parentSetIntersect.child(i);
if (canMerge(parentSetIntersect, child)) {
LogicalIntersect childIntersect = (LogicalIntersect) child;
newChildren.addAll(childIntersect.children());
List<Integer> outputOrder = getChildOutputOrder(parentSetIntersect, i, childIntersect);
for (List<SlotReference> outputs : childIntersect.getRegularChildrenOutputs()) {
newChildrenOutputs.add(reorderOutputs(outputs, outputOrder));
}
} else {
newChildren.add(child);
newChildrenOutputs.add(parentSetIntersect.getRegularChildOutput(i));
}
}
return parentSetIntersect.withChildrenAndTheirOutputs(
newChildren.build(), newChildrenOutputs.build());
}
static List<Integer> getChildOutputOrder(
LogicalSetOperation parent, int childIndex, LogicalSetOperation child) {
Map<ExprId, Integer> outputIndexes = new HashMap<>();
for (int i = 0; i < child.getOutput().size(); i++) {
outputIndexes.put(child.getOutput().get(i).getExprId(), i);
}
ImmutableList.Builder<Integer> order = ImmutableList.builder();
for (SlotReference slot : parent.getRegularChildOutput(childIndex)) {
Integer index = outputIndexes.get(slot.getExprId());
Preconditions.checkState(index != null, "Set operation input must belong to child output");
order.add(index);
}
return order.build();
}
static <T> List<T> reorderOutputs(List<T> outputs, List<Integer> order) {
ImmutableList.Builder<T> reordered = ImmutableList.builderWithExpectedSize(order.size());
for (int index : order) {
reordered.add(outputs.get(index));
}
return reordered.build();
}
/** canMerge */
public static boolean canMerge(LogicalSetOperation parent) {
Plan left = parent.child(0);
Plan right = parent.child(1);
return canMerge(parent, left) || canMerge(parent, right);
}
public static boolean canMerge(LogicalSetOperation parent, Plan child) {
return child.getClass().equals(parent.getClass())
&& isSameQualifierOrChildQualifierIsAll(parent, (LogicalSetOperation) child);
}
public static boolean isSameQualifierOrChildQualifierIsAll(LogicalSetOperation parentSetOperation,
LogicalSetOperation childSetOperation) {
return parentSetOperation.getQualifier() == childSetOperation.getQualifier()
|| childSetOperation.getQualifier() == Qualifier.ALL;
}
}