implement intersect and except related optimization rule: MergeIntersect, MergeExcept, RemoveEmptyIntersectBranchs, EvaluateEmptyIntersect, PruneIntersectSourceColumns, PruneExceptSourceColmns (#16761)
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/EvaluateEmptyIntersect.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/EvaluateEmptyIntersect.java
new file mode 100644
index 0000000..03c8bba
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/EvaluateEmptyIntersect.java
@@ -0,0 +1,63 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Assignments;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Lookup;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntersectNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.intersect;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.QueryCardinalityUtil.isEmpty;
+
+public class EvaluateEmptyIntersect implements Rule<IntersectNode> {
+
+ private static final Pattern<IntersectNode> PATTERN = intersect();
+
+ @Override
+ public Pattern<IntersectNode> getPattern() {
+ return PATTERN;
+ }
+
+ /** if any child of the intersect node is empty set, then the result set is empty */
+ @Override
+ public Result apply(IntersectNode node, Captures captures, Context context) {
+
+ Lookup lookup = context.getLookup();
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ if (isEmpty(node.getChildren().get(i), lookup)) {
+
+ // replace the intersect node with project node, append the empty node to the project node
+ Assignments.Builder assignments = Assignments.builder();
+ for (Symbol symbol : node.getOutputSymbols()) {
+ assignments.put(symbol, node.getSymbolMapping().get(symbol).get(i).toSymbolReference());
+ }
+ return Result.ofPlanNode(
+ new ProjectNode(node.getPlanNodeId(), node.getChildren().get(i), assignments.build()));
+ }
+ }
+
+ return Result.empty();
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeExcept.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeExcept.java
new file mode 100644
index 0000000..08fb908
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeExcept.java
@@ -0,0 +1,47 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExceptNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SetOperationNode;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import java.util.Optional;
+
+public class MergeExcept implements Rule<ExceptNode> {
+
+ private final Pattern<ExceptNode> pattern = Patterns.except();
+
+ @Override
+ public Pattern<ExceptNode> getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Result apply(ExceptNode node, Captures captures, Context context) {
+
+ SetOperationMerge mergeOperation = new SetOperationMerge(node, context);
+ Optional<SetOperationNode> result = mergeOperation.mergeFirstSource();
+ return result.map(Result::ofPlanNode).orElseGet(Result::empty);
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeIntersect.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeIntersect.java
new file mode 100644
index 0000000..1786909
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/MergeIntersect.java
@@ -0,0 +1,47 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntersectNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SetOperationNode;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import java.util.Optional;
+
+public class MergeIntersect implements Rule<IntersectNode> {
+
+ private final Pattern<IntersectNode> pattern = Patterns.intersect();
+
+ @Override
+ public Pattern<IntersectNode> getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Result apply(IntersectNode node, Captures captures, Context context) {
+
+ SetOperationMerge mergeOperation = new SetOperationMerge(node, context);
+ Optional<SetOperationNode> result = mergeOperation.merge();
+ return result.map(Result::ofPlanNode).orElseGet(Result::empty);
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneExceptSourceColumns.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneExceptSourceColumns.java
new file mode 100644
index 0000000..cc21065
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneExceptSourceColumns.java
@@ -0,0 +1,54 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExceptNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Util.restrictChildOutputs;
+
+public class PruneExceptSourceColumns implements Rule<ExceptNode> {
+
+ @Override
+ public Pattern<ExceptNode> getPattern() {
+ return Patterns.except();
+ }
+
+ @Override
+ public Result apply(ExceptNode node, Captures captures, Context context) {
+
+ @SuppressWarnings("unchecked")
+ Set<Symbol>[] referencedInputs = new Set[node.getChildren().size()];
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ referencedInputs[i] = ImmutableSet.copyOf(node.sourceOutputLayout(i));
+ }
+ return restrictChildOutputs(context.getIdAllocator(), node, referencedInputs)
+ .map(Rule.Result::ofPlanNode)
+ .orElse(Rule.Result.empty());
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneIntersectSourceColumns.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneIntersectSourceColumns.java
new file mode 100644
index 0000000..9fabb9b
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneIntersectSourceColumns.java
@@ -0,0 +1,55 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntersectNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Util.restrictChildOutputs;
+
+public class PruneIntersectSourceColumns implements Rule<IntersectNode> {
+
+ @Override
+ public Pattern<IntersectNode> getPattern() {
+ return Patterns.intersect();
+ }
+
+ @Override
+ public Result apply(IntersectNode node, Captures captures, Context context) {
+
+ @SuppressWarnings("unchecked")
+ Set<Symbol>[] referencedInputs = new Set[node.getChildren().size()];
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ referencedInputs[i] = ImmutableSet.copyOf(node.sourceOutputLayout(i));
+ }
+
+ return restrictChildOutputs(context.getIdAllocator(), node, referencedInputs)
+ .map(Rule.Result::ofPlanNode)
+ .orElse(Rule.Result.empty());
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/RemoveEmptyExceptBranches.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/RemoveEmptyExceptBranches.java
new file mode 100644
index 0000000..b07531e
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/RemoveEmptyExceptBranches.java
@@ -0,0 +1,139 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Assignments;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Lookup;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExceptNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode.singleAggregation;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode.singleGroupingSet;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.QueryCardinalityUtil.isEmpty;
+
+public class RemoveEmptyExceptBranches implements Rule<ExceptNode> {
+ private final Pattern<ExceptNode> pattern = Patterns.except();
+
+ @Override
+ public Pattern<ExceptNode> getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Result apply(ExceptNode node, Captures captures, Context context) {
+
+ // pre-check
+ Lookup lookup = context.getLookup();
+ ImmutableList.Builder<Boolean> emptyFlagsBuilder = ImmutableList.builder();
+ boolean anyEmpty = false;
+ for (PlanNode child : node.getChildren()) {
+ boolean isEmpty = isEmpty(child, lookup);
+ anyEmpty |= isEmpty;
+ emptyFlagsBuilder.add(isEmpty);
+ }
+ if (!anyEmpty) {
+ return Result.empty();
+ }
+
+ ImmutableList<Boolean> emptyFlags = emptyFlagsBuilder.build();
+ // if the first child of Except is empty set, replace the Except Node with Project node,
+ // and let the first child append to the Project node
+ if (emptyFlags.get(0)) {
+ Assignments.Builder assignments = Assignments.builder();
+ for (Symbol symbol : node.getOutputSymbols()) {
+ assignments.put(symbol, node.getSymbolMapping().get(symbol).get(0).toSymbolReference());
+ }
+ return Result.ofPlanNode(
+ new ProjectNode(node.getPlanNodeId(), node.getChildren().get(0), assignments.build()));
+ }
+
+ boolean hasEmptyBranches = false;
+ ImmutableList.Builder<PlanNode> newSourcesBuilder = ImmutableList.builder();
+ ImmutableListMultimap.Builder<Symbol, Symbol> outputsToInputsBuilder =
+ ImmutableListMultimap.builder();
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ PlanNode source = node.getChildren().get(i);
+ // first source is the set we're excluding rows from, so treat it separately
+ if (i == 0 || !emptyFlags.get(i)) {
+ newSourcesBuilder.add(source);
+
+ for (Symbol symbol : node.getOutputSymbols()) {
+ outputsToInputsBuilder.put(symbol, node.getSymbolMapping().get(symbol).get(i));
+ }
+ } else {
+ hasEmptyBranches = true;
+ }
+ }
+
+ if (!hasEmptyBranches) {
+ return Result.empty();
+ }
+
+ List<PlanNode> newSources = newSourcesBuilder.build();
+ ListMultimap<Symbol, Symbol> outputsToInputs = outputsToInputsBuilder.build();
+
+ // if the first child of Except node is non-empty set and other children of the Except node are
+ // empty set, replace the Except node with project node, add the aggregation node above the
+ // project node if Except that with distinct
+ if (newSources.size() == 1) {
+ Assignments.Builder assignments = Assignments.builder();
+ for (Map.Entry<Symbol, Symbol> entry : outputsToInputs.entries()) {
+ assignments.put(entry.getKey(), entry.getValue().toSymbolReference());
+ }
+
+ ProjectNode projectNode =
+ new ProjectNode(
+ context.getIdAllocator().genPlanNodeId(), newSources.get(0), assignments.build());
+
+ if (node.isDistinct()) {
+ return Result.ofPlanNode(
+ singleAggregation(
+ context.getIdAllocator().genPlanNodeId(),
+ projectNode,
+ ImmutableMap.of(),
+ singleGroupingSet(node.getOutputSymbols())));
+ }
+
+ return Result.ofPlanNode(projectNode);
+ }
+
+ return Result.ofPlanNode(
+ new ExceptNode(
+ node.getPlanNodeId(),
+ newSources,
+ outputsToInputs,
+ node.getOutputSymbols(),
+ node.isDistinct()));
+ }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/SetOperationMerge.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/SetOperationMerge.java
index 563a388..04dee38 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/SetOperationMerge.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/SetOperationMerge.java
@@ -23,6 +23,8 @@
import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Lookup;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExceptNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntersectNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SetOperationNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
@@ -50,11 +52,13 @@
this.newSources = new ArrayList<>();
}
- // Merge multiple union into one union
+ /** Merge multiple union into one union or multiple intersect into one intersect */
public Optional<SetOperationNode> merge() {
checkState(
- node instanceof UnionNode, "unexpected node type: %s", node.getClass().getSimpleName());
+ node instanceof UnionNode || node instanceof IntersectNode,
+ "unexpected node type: %s",
+ node.getClass().getSimpleName());
Lookup lookup = context.getLookup();
// Pre-check
boolean anyMerge =
@@ -65,22 +69,26 @@
return Optional.empty();
}
- List<PlanNode> childrenOfUnion =
+ List<PlanNode> children =
node.getChildren().stream().map(lookup::resolve).collect(toImmutableList());
ImmutableListMultimap.Builder<Symbol, Symbol> newMappingsBuilder =
ImmutableListMultimap.builder();
+ boolean resultIsDistinct = false;
boolean rewritten = false;
- for (int i = 0; i < childrenOfUnion.size(); i++) {
- PlanNode child = childrenOfUnion.get(i);
+ for (int i = 0; i < children.size(); i++) {
+ PlanNode child = children.get(i);
// Determine if set operations can be merged and whether the resulting set operation is
// quantified DISTINCT or ALL
- Optional<Boolean> mergedQuantifier = mergedQuantifierIsDistinct(node, child);
+ Optional<Boolean> mergedQuantifier = judgeQuantifierForUnionAndIntersect(node, child);
if (mergedQuantifier.isPresent()) {
addMergedMappings((SetOperationNode) child, i, newMappingsBuilder);
+ // for intersect : as long as one of intersect node is (intersect distinct), the merged
+ // intersect would be (intersect distinct).
+ resultIsDistinct = resultIsDistinct || mergedQuantifier.get();
rewritten = true;
} else {
// Keep mapping as it is
@@ -92,10 +100,56 @@
return Optional.empty();
}
- // the union has merged
+ if (node instanceof UnionNode) {
+ return Optional.of(
+ new UnionNode(
+ node.getPlanNodeId(),
+ newSources,
+ newMappingsBuilder.build(),
+ node.getOutputSymbols()));
+ }
+
return Optional.of(
- new UnionNode(
- node.getPlanNodeId(), newSources, newMappingsBuilder.build(), node.getOutputSymbols()));
+ new IntersectNode(
+ node.getPlanNodeId(),
+ newSources,
+ newMappingsBuilder.build(),
+ node.getOutputSymbols(),
+ resultIsDistinct));
+ }
+
+ /** only for except node, only merge first source node */
+ public Optional<SetOperationNode> mergeFirstSource() {
+
+ checkState(
+ node instanceof ExceptNode, "unexpected node type: %s", node.getClass().getSimpleName());
+
+ Lookup lookup = context.getLookup();
+
+ List<PlanNode> children =
+ node.getChildren().stream().map(lookup::resolve).collect(toImmutableList());
+ PlanNode firstChild = children.get(0);
+ Optional<Boolean> mergedQuantifier = judgeQuantifierForExcept((ExceptNode) node, firstChild);
+ if (!mergedQuantifier.isPresent()) {
+ return Optional.empty();
+ }
+
+ ImmutableListMultimap.Builder<Symbol, Symbol> newMappingsBuilder =
+ ImmutableListMultimap.builder();
+ addMergedMappings((SetOperationNode) firstChild, 0, newMappingsBuilder);
+ // Keep remaining as it is
+ for (int i = 1; i < children.size(); i++) {
+ PlanNode child = children.get(i);
+ addOriginalMappings(child, i, newMappingsBuilder);
+ }
+
+ return Optional.of(
+ new ExceptNode(
+ node.getPlanNodeId(),
+ newSources,
+ newMappingsBuilder.build(),
+ node.getOutputSymbols(),
+ mergedQuantifier.get()));
}
private void addMergedMappings(
@@ -126,9 +180,24 @@
/**
* Check if node and child are mergeable based on their set operation type and quantifier.
*
+ * <p>For parent and child of type UNION, merge is always possible and the assumed quantifier is
+ * ALL, because UnionNode always represents UNION ALL.
+ *
+ * <p>For parent and child of type INTERSECT, merge is always possible: 1. If parent and child are
+ * both INTERSECT ALL, the resulting set operation is INTERSECT ALL. 2. Otherwise, the resulting
+ * set operation is INTERSECT DISTINCT: a. If the parent is DISTINCT, the result has unique
+ * values, regardless of whether child branches were DISTINCT or ALL. b. If the child is DISTINCT,
+ * that branch is guaranteed to have unique values, so at most one element of the other branches
+ * will be retained -- this is equivalent to just doing DISTINCT on the parent.
+ *
* <p>Optional.empty() indicates that merge is not possible.
+ *
+ * <p>Optional.of(false) indicates that merged node with quantifier which is all.
+ *
+ * <p>Optional.of(true) indicates that merged node with quantifier which is distinct.
*/
- private Optional<Boolean> mergedQuantifierIsDistinct(SetOperationNode node, PlanNode child) {
+ private Optional<Boolean> judgeQuantifierForUnionAndIntersect(
+ SetOperationNode node, PlanNode child) {
if (!node.getClass().equals(child.getClass())) {
return Optional.empty();
@@ -138,7 +207,37 @@
return Optional.of(false);
}
- // the Judgment logic for intersect and except wait for supplying
- return Optional.empty();
+ if (node instanceof IntersectNode) {
+ if (!((IntersectNode) node).isDistinct() && !((IntersectNode) child).isDistinct()) {
+ return Optional.of(false);
+ }
+ return Optional.of(true);
+ }
+
+ // never should reach here
+ throw new IllegalStateException(
+ "unexpected setOperation node type: " + node.getClass().getSimpleName());
+ }
+
+ /**
+ * Check if node and child are mergeable based on their set operation type and quantifier.
+ *
+ * <p>For parent and child of type EXCEPT: 1. if parent is EXCEPT DISTINCT and child is EXCEPT
+ * ALL, merge is not possible 2. if parent and child are both EXCEPT DISTINCT, the resulting set
+ * operation is EXCEPT DISTINCT 3. if parent and child are both EXCEPT ALL, the resulting set
+ * operation is EXCEPT ALL 4. if parent is EXCEPT ALL and child is EXCEPT DISTINCT, the resulting
+ * set operation is EXCEPT DISTINCT
+ */
+ private Optional<Boolean> judgeQuantifierForExcept(ExceptNode node, PlanNode child) {
+
+ if (!node.getClass().equals(child.getClass())) {
+ return Optional.empty();
+ }
+
+ if (node.isDistinct() && !((ExceptNode) child).isDistinct()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(((ExceptNode) child).isDistinct());
}
}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
index d7084c3..1c73274 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
@@ -26,6 +26,7 @@
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.RuleStatsRecorder;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.CanonicalizeExpressions;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.EvaluateEmptyIntersect;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.ImplementExceptAll;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.ImplementExceptDistinctAsUnion;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.ImplementIntersectAll;
@@ -33,7 +34,9 @@
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.ImplementPatternRecognition;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.ImplementTableFunctionSource;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.InlineProjections;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeExcept;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeFilters;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeIntersect;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeLimitOverProjectWithSort;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeLimitWithSort;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.MergeLimits;
@@ -50,10 +53,12 @@
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneCorrelatedJoinCorrelation;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneDistinctAggregation;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneEnforceSingleRowColumns;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneExceptSourceColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneExplainAnalyzeColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneFillColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneFilterColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneGapFillColumns;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneIntersectSourceColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneJoinChildrenColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneJoinColumns;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneLimitColumns;
@@ -76,6 +81,7 @@
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushProjectionThroughUnion;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushTopKThroughUnion;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.RemoveDuplicateConditions;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.RemoveEmptyExceptBranches;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.RemoveEmptyUnionBranches;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.RemoveRedundantEnforceSingleRowNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.RemoveRedundantExists;
@@ -126,7 +132,9 @@
new PruneCorrelatedJoinColumns(),
new PruneCorrelatedJoinCorrelation(),
new PruneEnforceSingleRowColumns(),
+ new PruneExceptSourceColumns(),
new PruneFilterColumns(),
+ new PruneIntersectSourceColumns(),
new PruneGapFillColumns(),
new PruneFillColumns(),
new PruneLimitColumns(),
@@ -216,6 +224,8 @@
ImmutableSet.of(
new ImplementTableFunctionSource(),
new RemoveEmptyUnionBranches(),
+ new EvaluateEmptyIntersect(),
+ new RemoveEmptyExceptBranches(),
new MergeFilters(),
new InlineProjections(plannerContext),
new RemoveRedundantIdentityProjections(),
@@ -258,8 +268,10 @@
.addAll(limitPushdownRules)
.addAll(
ImmutableSet.of(
- new MergeUnion(),
new RemoveEmptyUnionBranches(),
+ new EvaluateEmptyIntersect(),
+ new RemoveEmptyExceptBranches(),
+ new MergeUnion(),
new MergeFilters(),
new RemoveTrivialFilters(),
new MergeLimits(),
@@ -275,8 +287,8 @@
.addAll(
ImmutableSet.of(
new MergeUnion(),
- // new MergeIntersect
- // new MergeExcept
+ new MergeIntersect(),
+ new MergeExcept(),
new PruneDistinctAggregation()))
.build()),
new IterativeOptimizer(
@@ -336,11 +348,17 @@
// Currently, we inline symbols but do not simplify them in predicate push down.
// So we have to add extra simplifyOptimizer here
simplifyOptimizer,
- // Currently, Distinct is not supported, so we cant use this rule for now.
- // new IterativeOptimizer(
- // plannerContext,
- // ruleStats,
- // ImmutableSet.of(new TransformFilteringSemiJoinToInnerJoin())),
+ new IterativeOptimizer(
+ plannerContext,
+ ruleStats,
+ ImmutableSet.of(
+ new RemoveEmptyUnionBranches(),
+ new EvaluateEmptyIntersect(),
+ new RemoveEmptyExceptBranches()
+ // Currently, Distinct is not supported, so we cant use this rule for now.
+ // new TransformFilteringSemiJoinToInnerJoin()
+ )),
+
// redo columnPrune and inlineProjections after pushPredicateIntoTableScan
columnPruningOptimizer,
inlineProjectionLimitFiltersOptimizer,
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/EvaluateEmptyIntersectTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/EvaluateEmptyIntersectTest.java
new file mode 100644
index 0000000..a69b4af
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/EvaluateEmptyIntersectTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.analyzer;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.Test;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.limit;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScan;
+
+public class EvaluateEmptyIntersectTest {
+
+ @Test
+ public void EvaluateEmptyIntersectTest() {
+ // Normal case
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1 limit 0) intersect (select tag1 from t2) intersect (select tag1 from t3)"),
+ output((limit(0, tableScan("testdb.t1")))));
+
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1 ) intersect all (select tag1 from t2 limit 0) intersect all (select tag1 from t3)"),
+ output((limit(0, tableScan("testdb.t2")))));
+ }
+}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeExceptTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeExceptTest.java
new file mode 100644
index 0000000..41275bf
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeExceptTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.analyzer;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.Test;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.aggregation;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.filter;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.project;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.sort;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.window;
+
+/**
+ * for except node, only merge first source node
+ *
+ * <p>For parent and child of type EXCEPT: 1. if parent is EXCEPT DISTINCT and child is EXCEPT ALL,
+ * merge is not possible 2. if parent and child are both EXCEPT DISTINCT, the resulting set
+ * operation is EXCEPT DISTINCT 3. if parent and child are both EXCEPT ALL, the resulting set
+ * operation is EXCEPT ALL 4. if parent is EXCEPT ALL and child is EXCEPT DISTINCT, the resulting
+ * set operation is EXCEPT DISTINCT
+ */
+public class MergeExceptTest {
+
+ @Test
+ public void mergeTwoExceptAll() {
+ PlanTester planTester = new PlanTester();
+
+ // the parent
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 except all select tag1 from t2 except all select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ project(
+ window(
+ sort(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))))));
+ }
+
+ @Test
+ public void mergeTwoExceptDistinct() {
+ PlanTester planTester = new PlanTester();
+
+ // the parent
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 except distinct select tag1 from t2 except distinct select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))));
+ }
+
+ @Test
+ public void mergeExceptAllAndExceptDistinct() {
+ PlanTester planTester = new PlanTester();
+
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 except distinct select tag1 from t2 except all select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))));
+ }
+
+ @Test
+ public void testMergeNotPossibleForDistinctOverAll() {
+ PlanTester planTester = new PlanTester();
+
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 except all select tag1 from t2 except select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(
+ project(
+ filter(
+ project(
+ window(
+ sort(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2"))))))))),
+ project(tableScan("testdb.t3"))))))));
+ }
+
+ @Test
+ public void testNoMergeForRightChild() {
+ PlanTester planTester = new PlanTester();
+
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 except (select tag1 from t2 except select tag1 from t3) "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3")))))))))))));
+ }
+}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeIntersectTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeIntersectTest.java
new file mode 100644
index 0000000..caaff83
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/MergeIntersectTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.analyzer;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.Test;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.aggregation;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.filter;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.project;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.sort;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.window;
+
+/**
+ * For parent and child of type INTERSECT, merge is always possible: 1. if parent and child are both
+ * INTERSECT ALL, the resulting set operation is INTERSECT ALL 2. otherwise, the resulting set
+ * operation is INTERSECT DISTINCT: 3. if the parent is DISTINCT, the result has unique values,
+ * regardless of whether child branches were DISTINCT or ALL, 4. if the child is DISTINCT, that
+ * branch is guaranteed to have unique values, so at most one element of the other branches will be
+ * retained -- this is equivalent to just doing DISTINCT on the parent.
+ */
+public class MergeIntersectTest {
+
+ @Test
+ public void mergeTwoIntersectAll() {
+ PlanTester planTester = new PlanTester();
+
+ // the parent
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 intersect all select tag1 from t2 intersect all select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ project(
+ window(
+ sort(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))))));
+ }
+
+ @Test
+ public void mergeTwoIntersectDistinct() {
+ PlanTester planTester = new PlanTester();
+
+ // the parent
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 intersect distinct select tag1 from t2 intersect distinct select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))));
+ }
+
+ @Test
+ public void mergeIntersectAllAndIntersectDistinct() {
+ PlanTester planTester = new PlanTester();
+
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 intersect all select tag1 from t2 intersect distinct select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))));
+
+ assertPlan(
+ planTester.createPlan(
+ "select tag1 from t1 intersect select tag1 from t2 intersect all select tag1 from t3 "),
+ output(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")),
+ project(tableScan("testdb.t3"))))))));
+ }
+}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/RemoveEmptyExceptBranchesTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/RemoveEmptyExceptBranchesTest.java
new file mode 100644
index 0000000..914a9bd
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/RemoveEmptyExceptBranchesTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.analyzer;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.Test;
+
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.aggregation;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.filter;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.limit;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.project;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.sort;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScan;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+import static org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.window;
+
+public class RemoveEmptyExceptBranchesTest {
+
+ @Test
+ public void removeEmptyExceptBranchesTest1() {
+ // Normal case
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1 limit 0) except (select tag1 from t2) except (select tag1 from t3)"),
+ output((limit(0, tableScan("testdb.t1")))));
+
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1 limit 0) except all (select tag1 from t2) except all (select tag1 from t3)"),
+ output((limit(0, tableScan("testdb.t1")))));
+ }
+
+ @Test
+ public void removeEmptyExceptBranchesTest2() {
+ // Normal case
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1 ) except (select tag1 from t2 limit 0) except (select tag1 from t3 limit 0)"),
+ output(aggregation(tableScan("testdb.t1"))));
+
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1) except all (select tag1 from t2 limit 0) except all (select tag1 from t3 limit 0)"),
+ output((tableScan("testdb.t1"))));
+ }
+
+ @Test
+ public void removeEmptyExceptBranchesTest3() {
+ // Normal case
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1) except (select tag1 from t2) except (select tag1 from t3 limit 0)"),
+ output(
+ aggregation(
+ project(
+ filter(
+ aggregation(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2")))))))));
+
+ assertPlan(
+ new PlanTester()
+ .createPlan(
+ "(select tag1 from t1) except all (select tag1 from t2 ) except all (select tag1 from t3 limit 0)"),
+ output(
+ project(
+ filter(
+ project(
+ window(
+ sort(
+ union(
+ project(tableScan("testdb.t1")),
+ project(tableScan("testdb.t2"))))))))));
+ }
+}