This is an automated email from the ASF dual-hosted git repository.

jackietien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new fbcca989b37 Support Union in TableModel
fbcca989b37 is described below

commit fbcca989b37d1f3236510417259575b2db49fa0e
Author: Weihao Li <[email protected]>
AuthorDate: Thu Aug 21 16:28:52 2025 +0800

    Support Union in TableModel
---
 .../it/query/recent/IoTDBUnionTableIT.java         | 127 +++++++++++++++++++
 .../plan/planner/TableOperatorGenerator.java       |  17 +++
 .../plan/planner/plan/node/PlanGraphPrinter.java   |   9 ++
 .../plan/planner/plan/node/PlanNodeType.java       |   4 +
 .../plan/planner/plan/node/PlanVisitor.java        |   5 +
 .../relational/analyzer/StatementAnalyzer.java     |   6 +-
 .../plan/relational/planner/QueryPlanner.java      |  34 +++++
 .../plan/relational/planner/RelationPlanner.java   |  90 ++++++++++++-
 .../distribute/TableDistributedPlanGenerator.java  |   7 ++
 .../iterative/rule/PruneDistinctAggregation.java   |   6 +
 .../planner/iterative/rule/PruneUnionColumns.java  |  80 ++++++++++++
 .../iterative/rule/PruneUnionSourceColumns.java    |  52 ++++++++
 .../plan/relational/planner/node/Patterns.java     |   4 +
 .../relational/planner/node/SetOperationNode.java  | 140 +++++++++++++++++++++
 .../plan/relational/planner/node/UnionNode.java    |  97 ++++++++++++++
 .../optimizations/LogicalOptimizeFactory.java      |   6 +-
 .../optimizations/PushPredicateIntoTableScan.java  |  27 ++++
 .../TransformAggregationToStreamable.java          |   6 +
 .../optimizations/TransformSortToStreamSort.java   |   7 ++
 .../optimizations/UnaliasSymbolReferences.java     |  54 ++++++++
 .../plan/relational/type/CompatibleResolver.java   | 108 ++++++++++++++++
 .../plan/relational/analyzer/UnionTest.java        |  65 ++++++++++
 .../planner/assertions/PlanMatchPattern.java       |   5 +
 23 files changed, 949 insertions(+), 7 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
new file mode 100644
index 00000000000..90ed5f04332
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
@@ -0,0 +1,127 @@
+/*
+ * 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.relational.it.query.recent;
+
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
+import static org.apache.iotdb.db.it.utils.TestUtils.tableAssertTestFail;
+import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
+public class IoTDBUnionTableIT {
+  protected static final String DATABASE_NAME = "test";
+  protected static final String[] createSqls =
+      new String[] {
+        "CREATE DATABASE " + DATABASE_NAME,
+        "USE " + DATABASE_NAME,
+        "create table table1(device STRING TAG, s1 INT32 FIELD, s2 INT32 
FIELD)",
+        "insert into table1 values (1, 'd1', 1, 1)",
+        "insert into table1 values (2, 'd1', 2, 2)",
+        "create table table2(device STRING TAG, s1 INT64 FIELD, s2 DOUBLE 
FIELD)",
+        "insert into table2 values (1, 'd1', 1, 1.0)",
+        "insert into table2 values (3, 'd1', 3, 3.0)",
+        "create table table3(device STRING TAG, s1_testName INT64 FIELD, 
s2_testName DOUBLE FIELD)",
+        "insert into table3 values (1, 'd1', 1, 1.0)",
+        "insert into table3 values (3, 'd1', 3, 3.0)",
+        "create table table4(device STRING TAG, s1 TEXT FIELD, s2 DOUBLE 
FIELD)"
+      };
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    EnvFactory.getEnv().initClusterEnvironment();
+    prepareTableData(createSqls);
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void normalTest() {
+    String[] expectedHeader = new String[] {"time", "device", "s1", "s2"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+          "1970-01-01T00:00:00.002Z,d1,2,2.0,",
+          "1970-01-01T00:00:00.003Z,d1,3,3.0,"
+        };
+    tableResultSetEqualTest(
+        "(select * from table1) union (select * from table2) order by time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+    tableResultSetEqualTest(
+        "(select * from table1) union distinct (select * from table2) order by 
time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+          "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+          "1970-01-01T00:00:00.002Z,d1,2,2.0,",
+          "1970-01-01T00:00:00.003Z,d1,3,3.0,"
+        };
+    tableResultSetEqualTest(
+        "(select * from table1) union all (select * from table2) order by 
time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+    tableResultSetEqualTest(
+        "(select * from table1) union all (select * from table3) order by 
time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    // result correction test for union with predicate
+    retArray =
+        new String[] {"1970-01-01T00:00:00.002Z,d1,2,2.0,", 
"1970-01-01T00:00:00.003Z,d1,3,3.0,"};
+    tableResultSetEqualTest(
+        "select * from ((select * from table1) union all (select * from 
table2)) where s1>1 order by time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
+  @Test
+  public void exceptionTest() {
+    tableAssertTestFail(
+        "(select * from table1) union all (select * from table4)",
+        "has incompatible types: INT32, TEXT",
+        DATABASE_NAME);
+    tableAssertTestFail(
+        "(select * from table1) union all (select time from table4)",
+        "UNION query has different number of fields: 4, 1",
+        DATABASE_NAME);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
index 475732a6af5..0dd59d84d6b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
@@ -210,6 +210,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableDeviceFetchNode;
@@ -4060,4 +4061,20 @@ public class TableOperatorGenerator extends 
PlanVisitor<Operator, LocalExecution
     return new WindowAggregator(
         accumulator, getTSDataType(typeProvider.getTableModelType(symbol)), 
argumentChannels);
   }
+
+  @Override
+  public Operator visitUnion(UnionNode node, LocalExecutionPlanContext 
context) {
+    List<Operator> children =
+        node.getChildren().stream()
+            .map(child -> child.accept(this, context))
+            .collect(Collectors.toList());
+    OperatorContext operatorContext =
+        context
+            .getDriverContext()
+            .addOperatorContext(
+                context.getNextOperatorId(),
+                node.getPlanNodeId(),
+                CollectOperator.class.getSimpleName());
+    return new CollectOperator(operatorContext, children);
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
index 776adf96cb7..b07cc767910 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
@@ -82,6 +82,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SemiJoinNode
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 
@@ -1094,6 +1095,14 @@ public class PlanGraphPrinter extends 
PlanVisitor<List<String>, PlanGraphPrinter
             : String.valueOf(regionReplicaSet.getRegionId().id));
   }
 
+  @Override
+  public List<String> visitUnion(UnionNode node, GraphContext context) {
+    List<String> boxValue = new ArrayList<>();
+    boxValue.add(String.format("Union-%s", node.getPlanNodeId().getId()));
+    boxValue.add(String.format("OutputSymbols: %s", node.getOutputSymbols()));
+    return render(node, boxValue, context);
+  }
+
   private List<String> render(PlanNode node, List<String> nodeBoxString, 
GraphContext context) {
     Box box = new Box(nodeBoxString);
     List<List<String>> children = new ArrayList<>();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
index f7459a0eb57..a834f7e076d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
@@ -130,6 +130,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctio
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.ConstructTableDevicesBlackListNode;
@@ -308,6 +309,7 @@ public enum PlanNodeType {
   TABLE_PATTERN_RECOGNITION_NODE((short) 1031),
   TABLE_WINDOW_FUNCTION((short) 1032),
   TABLE_INTO_NODE((short) 1033),
+  TABLE_UNION_NODE((short) 1034),
 
   RELATIONAL_INSERT_TABLET((short) 2000),
   RELATIONAL_INSERT_ROW((short) 2001),
@@ -693,6 +695,8 @@ public enum PlanNodeType {
       case 1033:
         return 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntoNode.deserialize(
             buffer);
+      case 1034:
+        return UnionNode.deserialize(buffer);
       case 2000:
         return RelationalInsertTabletNode.deserialize(buffer);
       case 2001:
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
index 9ac845015e3..b884ac01935 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
@@ -136,6 +136,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNod
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.ConstructTableDevicesBlackListNode;
@@ -830,4 +831,8 @@ public abstract class PlanVisitor<R, C> {
   public R visitPatternRecognition(PatternRecognitionNode node, C context) {
     return visitPlan(node, context);
   }
+
+  public R visitUnion(UnionNode node, C context) {
+    return visitPlan(node, context);
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
index 382895122a7..8a07a18f91b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
@@ -190,6 +190,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WindowSpecificati
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.With;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WithQuery;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WrappedInsertStatement;
+import org.apache.iotdb.db.queryengine.plan.relational.type.CompatibleResolver;
 import 
org.apache.iotdb.db.queryengine.plan.relational.type.InternalTypeManager;
 import org.apache.iotdb.db.queryengine.plan.relational.type.TypeManager;
 import org.apache.iotdb.db.queryengine.plan.statement.component.FillPolicy;
@@ -2974,7 +2975,9 @@ public class StatementAnalyzer {
         }
         for (int i = 0; i < descFieldSize; i++) {
           Type descFieldType = relationType.getFieldByIndex(i).getType();
-          if (descFieldType != outputFieldTypes[i]) {
+          Optional<Type> commonSuperType =
+              CompatibleResolver.getCommonSuperType(outputFieldTypes[i], 
descFieldType);
+          if (!commonSuperType.isPresent()) {
             throw new SemanticException(
                 String.format(
                     "column %d in %s query has incompatible types: %s, %s",
@@ -2983,6 +2986,7 @@ public class StatementAnalyzer {
                     outputFieldTypes[i].getDisplayName(),
                     descFieldType.getDisplayName()));
           }
+          outputFieldTypes[i] = commonSuperType.get();
         }
       }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
index a8b5f0a80e9..106b406257f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
@@ -1101,6 +1101,32 @@ public class QueryPlanner {
     return new PlanAndMappings(subPlan, mappings);
   }
 
+  public static NodeAndMappings coerce(
+      RelationPlan plan, List<Type> types, SymbolAllocator symbolAllocator, 
QueryId idAllocator) {
+    List<Symbol> visibleFields = visibleFields(plan);
+    checkArgument(visibleFields.size() == types.size());
+
+    Assignments.Builder assignments = Assignments.builder();
+    ImmutableList.Builder<Symbol> mappings = ImmutableList.builder();
+    for (int i = 0; i < types.size(); i++) {
+      Symbol input = visibleFields.get(i);
+      Type type = types.get(i);
+
+      if (!symbolAllocator.getTypes().getTableModelType(input).equals(type)) {
+        Symbol coerced = symbolAllocator.newSymbol(input.getName(), type);
+        assignments.put(coerced, new Cast(input.toSymbolReference(), 
toSqlType(type)));
+        mappings.add(coerced);
+      } else {
+        assignments.putIdentity(input);
+        mappings.add(input);
+      }
+    }
+
+    ProjectNode coerced =
+        new ProjectNode(idAllocator.genPlanNodeId(), plan.getRoot(), 
assignments.build());
+    return new NodeAndMappings(coerced, mappings.build());
+  }
+
   public static List<Symbol> visibleFields(RelationPlan subPlan) {
     RelationType descriptor = subPlan.getDescriptor();
     return descriptor.getAllFields().stream()
@@ -1110,6 +1136,14 @@ public class QueryPlanner {
         .collect(toImmutableList());
   }
 
+  public static NodeAndMappings pruneInvisibleFields(RelationPlan plan, 
QueryId idAllocator) {
+    List<Symbol> visibleFields = visibleFields(plan);
+    ProjectNode pruned =
+        new ProjectNode(
+            idAllocator.genPlanNodeId(), plan.getRoot(), 
Assignments.identity(visibleFields));
+    return new NodeAndMappings(pruned, visibleFields);
+  }
+
   public static OrderingScheme translateOrderingScheme(
       List<SortItem> items, Function<Expression, Symbol> coercions) {
     List<Symbol> coerced =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
index 55f65db3d53..b69a03e26e1 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
@@ -66,6 +66,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SkipToPositi
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.AggregationLabelSet;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.AggregationValuePointer;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.ClassifierValuePointer;
@@ -105,7 +106,9 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.PipeEnriched;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.QualifiedName;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Query;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.QuerySpecification;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Relation;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RowPattern;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SetOperation;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SkipTo;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SortItem;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SubqueryExpression;
@@ -121,9 +124,12 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement;
 import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
 import org.apache.tsfile.read.common.type.Type;
 import org.apache.tsfile.write.schema.MeasurementSchema;
 
@@ -152,7 +158,10 @@ import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.PlanBuilde
 import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.coerce;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.coerceIfNecessary;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.extractPatternRecognitionExpressions;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.pruneInvisibleFields;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.ir.IrUtils.extractPredicates;
+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.sql.ast.Join.Type.CROSS;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Join.Type.FULL;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Join.Type.IMPLICIT;
@@ -1094,6 +1103,64 @@ public class RelationPlanner extends 
AstVisitor<RelationPlan, Void> {
     }
   }
 
+  @Override
+  protected RelationPlan visitUnion(Union node, Void context) {
+    Preconditions.checkArgument(!node.getRelations().isEmpty(), "No relations 
specified for UNION");
+
+    SetOperationPlan setOperationPlan = process(node);
+
+    PlanNode planNode =
+        new UnionNode(
+            idAllocator.genPlanNodeId(),
+            setOperationPlan.getChildren(),
+            setOperationPlan.getSymbolMapping(),
+            
ImmutableList.copyOf(setOperationPlan.getSymbolMapping().keySet()));
+    if (node.isDistinct()) {
+      planNode = distinct(planNode);
+    }
+    return new RelationPlan(
+        planNode, analysis.getScope(node), planNode.getOutputSymbols(), 
outerContext);
+  }
+
+  private SetOperationPlan process(SetOperation node) {
+    RelationType outputFields = analysis.getOutputDescriptor(node);
+    List<Symbol> outputs =
+        outputFields.getAllFields().stream()
+            .map(symbolAllocator::newSymbol)
+            .collect(toImmutableList());
+
+    ImmutableListMultimap.Builder<Symbol, Symbol> symbolMapping = 
ImmutableListMultimap.builder();
+    ImmutableList.Builder<PlanNode> children = ImmutableList.builder();
+
+    for (Relation child : node.getRelations()) {
+      RelationPlan plan = process(child, null);
+
+      NodeAndMappings planAndMappings;
+      List<Type> types = analysis.getRelationCoercion(child);
+      if (types == null) {
+        // no coercion required, only prune invisible fields from child outputs
+        planAndMappings = pruneInvisibleFields(plan, idAllocator);
+      } else {
+        // apply required coercion and prune invisible fields from child 
outputs
+        planAndMappings = coerce(plan, types, symbolAllocator, idAllocator);
+      }
+      for (int i = 0; i < outputFields.getAllFields().size(); i++) {
+        symbolMapping.put(outputs.get(i), planAndMappings.getFields().get(i));
+      }
+
+      children.add(planAndMappings.getNode());
+    }
+    return new SetOperationPlan(children.build(), symbolMapping.build());
+  }
+
+  private PlanNode distinct(PlanNode node) {
+    return singleAggregation(
+        idAllocator.genPlanNodeId(),
+        node,
+        ImmutableMap.of(),
+        singleGroupingSet(node.getOutputSymbols()));
+  }
+
   // ================================ Implemented later 
=====================================
 
   @Override
@@ -1106,11 +1173,6 @@ public class RelationPlanner extends 
AstVisitor<RelationPlan, Void> {
     throw new IllegalStateException("Intersect is not supported in current 
version.");
   }
 
-  @Override
-  protected RelationPlan visitUnion(Union node, Void context) {
-    throw new IllegalStateException("Union is not supported in current 
version.");
-  }
-
   @Override
   protected RelationPlan visitExcept(Except node, Void context) {
     throw new IllegalStateException("Except is not supported in current 
version.");
@@ -1384,6 +1446,24 @@ public class RelationPlanner extends 
AstVisitor<RelationPlan, Void> {
     }
   }
 
+  private static final class SetOperationPlan {
+    private final List<PlanNode> children;
+    private final ListMultimap<Symbol, Symbol> symbolMapping;
+
+    private SetOperationPlan(List<PlanNode> children, ListMultimap<Symbol, 
Symbol> symbolMapping) {
+      this.children = children;
+      this.symbolMapping = symbolMapping;
+    }
+
+    public List<PlanNode> getChildren() {
+      return children;
+    }
+
+    public ListMultimap<Symbol, Symbol> getSymbolMapping() {
+      return symbolMapping;
+    }
+  }
+
   public static class PatternRecognitionComponents {
     private final Map<Symbol, Measure> measures;
     private final List<Symbol> measureOutputs;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
index 2d633eacd64..7d569b804fa 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
@@ -82,6 +82,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.AbstractTableDeviceQueryNode;
@@ -1758,6 +1759,12 @@ public class TableDistributedPlanGenerator
     }
   }
 
+  @Override
+  public List<PlanNode> visitUnion(UnionNode node, PlanContext context) {
+    context.clearExpectedOrderingScheme();
+    return visitMultiChildProcess(node, context);
+  }
+
   public static class PlanContext {
     final Map<PlanNodeId, NodeDistribution> nodeDistributionMap;
     boolean hasExchangeNode = false;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
index 0e9f55c025e..5b3a4101478 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
@@ -24,6 +24,7 @@ import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
 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.AggregationNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
 import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
 
@@ -94,6 +95,11 @@ public class PruneDistinctAggregation implements 
Rule<AggregationNode> {
       return rewriteChildren(node, false);
     }
 
+    @Override
+    public PlanNode visitUnion(UnionNode node, Boolean context) {
+      return rewriteChildren(node, context);
+    }
+
     /*@Override
     public PlanNode visitUnion(UnionNode node, Boolean context)
     {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
new file mode 100644
index 00000000000..d945ce117af
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
@@ -0,0 +1,80 @@
+/*
+ * 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.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static 
com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.union;
+
+/**
+ * Transforms
+ *
+ * <pre>
+ * - Project (a)
+ *      - Union
+ *        output mappings: {a->c, a->e, b->d, b->f}
+ *          - Source (c, d)
+ *          - Source (e, f)
+ * </pre>
+ *
+ * into:
+ *
+ * <pre>
+ * - Project (a)
+ *      - Union
+ *        output mappings: {a->c, a->e}
+ *          - Source (c, d)
+ *          - Source (e, f)
+ * </pre>
+ *
+ * Note: as a result of this rule, the UnionNode's sources are eligible for 
pruning outputs. This is
+ * accomplished by PruneUnionSourceColumns rule.
+ */
+public class PruneUnionColumns extends ProjectOffPushDownRule<UnionNode> {
+  public PruneUnionColumns() {
+    super(union());
+  }
+
+  @Override
+  protected Optional<PlanNode> pushDownProjectOff(
+      Context context, UnionNode unionNode, Set<Symbol> referencedOutputs) {
+    ImmutableListMultimap<Symbol, Symbol> prunedOutputMappings =
+        unionNode.getSymbolMapping().entries().stream()
+            .filter(entry -> referencedOutputs.contains(entry.getKey()))
+            .collect(toImmutableListMultimap(Map.Entry::getKey, 
Map.Entry::getValue));
+
+    return Optional.of(
+        new UnionNode(
+            unionNode.getPlanNodeId(),
+            unionNode.getChildren(),
+            prunedOutputMappings,
+            ImmutableList.copyOf(prunedOutputMappings.keySet())));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
new file mode 100644
index 00000000000..71760f58aaa
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
@@ -0,0 +1,52 @@
+/*
+ * 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.UnionNode;
+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;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.union;
+
+public class PruneUnionSourceColumns implements Rule<UnionNode> {
+  @Override
+  public Pattern<UnionNode> getPattern() {
+    return union();
+  }
+
+  @Override
+  public Result apply(UnionNode 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/node/Patterns.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
index b826b00ea93..3ebd933d28c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
@@ -185,6 +185,10 @@ public final class Patterns {
     return typeOf(PatternRecognitionNode.class);
   }
 
+  public static Pattern<UnionNode> union() {
+    return typeOf(UnionNode.class);
+  }
+
   /*public static Pattern<TableWriterNode> tableWriterNode()
   {
       return typeOf(TableWriterNode.class);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
new file mode 100644
index 00000000000..a23ba5f6cd4
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
@@ -0,0 +1,140 @@
+/*
+ * 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.node;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeId;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.MultiChildProcessNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SymbolReference;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+public abstract class SetOperationNode extends MultiChildProcessNode {
+  // Corresponding is not supported in UNION now, this field can be used for 
future expansion.
+  // We don't need to serialize this field now, consider it when support 
Corresponding.
+  private final transient ListMultimap<Symbol, Symbol> outputToInputs;
+  private final List<Symbol> outputs;
+
+  protected SetOperationNode(
+      PlanNodeId id,
+      List<PlanNode> children,
+      ListMultimap<Symbol, Symbol> outputToInputs,
+      List<Symbol> outputs) {
+    super(id);
+
+    requireNonNull(children, "children is null");
+    checkArgument(!children.isEmpty(), "Must have at least one source");
+    requireNonNull(outputToInputs, "outputToInputs is null");
+    requireNonNull(outputs, "outputs is null");
+
+    this.children = ImmutableList.copyOf(children);
+    this.outputToInputs = ImmutableListMultimap.copyOf(outputToInputs);
+    this.outputs = ImmutableList.copyOf(outputs);
+
+    for (Collection<Symbol> inputs : this.outputToInputs.asMap().values()) {
+      checkArgument(
+          inputs.size() == this.children.size(),
+          "Every child needs to map its symbols to an output %s operation 
symbol",
+          this.getClass().getSimpleName());
+    }
+
+    // Make sure each child positionally corresponds to their Symbol values in 
the Multimap
+    for (int i = 0; i < children.size(); i++) {
+      Set<Symbol> childSymbols = 
ImmutableSet.copyOf(children.get(i).getOutputSymbols());
+      for (Collection<Symbol> expectedInputs : 
this.outputToInputs.asMap().values()) {
+        checkArgument(
+            childSymbols.contains(Iterables.get(expectedInputs, i)),
+            "Child does not provide required symbols");
+      }
+    }
+  }
+
+  // used for clone(), we needn't check arguments again
+  protected SetOperationNode(
+      PlanNodeId id, ListMultimap<Symbol, Symbol> outputToInputs, List<Symbol> 
outputs) {
+    super(id);
+
+    this.outputToInputs = ImmutableListMultimap.copyOf(outputToInputs);
+    this.outputs = ImmutableList.copyOf(outputs);
+  }
+
+  @Override
+  public List<Symbol> getOutputSymbols() {
+    return outputs;
+  }
+
+  public ListMultimap<Symbol, Symbol> getSymbolMapping() {
+    return outputToInputs;
+  }
+
+  public List<Symbol> sourceOutputLayout(int sourceIndex) {
+    // Make sure the sourceOutputLayout symbols are listed in the same order 
as the corresponding
+    // output symbols
+    return getOutputSymbols().stream()
+        .map(symbol -> outputToInputs.get(symbol).get(sourceIndex))
+        .collect(toImmutableList());
+  }
+
+  /** Returns the output to input symbol mapping for the given source channel 
*/
+  public Map<Symbol, SymbolReference> sourceSymbolMap(int sourceIndex) {
+    ImmutableMap.Builder<Symbol, SymbolReference> builder = 
ImmutableMap.builder();
+    for (Map.Entry<Symbol, Collection<Symbol>> entry : 
outputToInputs.asMap().entrySet()) {
+      builder.put(entry.getKey(), Iterables.get(entry.getValue(), 
sourceIndex).toSymbolReference());
+    }
+
+    return builder.buildOrThrow();
+  }
+
+  /**
+   * Returns the input to output symbol mapping for the given source channel. 
A single input symbol
+   * can map to multiple output symbols, thus requiring a Multimap.
+   */
+  public Multimap<Symbol, SymbolReference> outputSymbolMap(int sourceIndex) {
+    return Multimaps.transformValues(
+        FluentIterable.from(getOutputSymbols())
+            .toMap(outputToSourceSymbolFunction(sourceIndex))
+            .asMultimap()
+            .inverse(),
+        Symbol::toSymbolReference);
+  }
+
+  private Function<Symbol, Symbol> outputToSourceSymbolFunction(int 
sourceIndex) {
+    return outputSymbol -> outputToInputs.get(outputSymbol).get(sourceIndex);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
new file mode 100644
index 00000000000..59fa1674368
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
@@ -0,0 +1,97 @@
+/*
+ * 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.node;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeType;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UnionNode extends SetOperationNode {
+  public UnionNode(
+      PlanNodeId id,
+      List<PlanNode> children,
+      ListMultimap<Symbol, Symbol> outputToInputs,
+      List<Symbol> outputs) {
+    super(id, children, outputToInputs, outputs);
+  }
+
+  private UnionNode(
+      PlanNodeId id, ListMultimap<Symbol, Symbol> outputToInputs, List<Symbol> 
outputs) {
+    super(id, outputToInputs, outputs);
+  }
+
+  @Override
+  public PlanNode clone() {
+    return new UnionNode(getPlanNodeId(), getSymbolMapping(), 
getOutputSymbols());
+  }
+
+  @Override
+  public List<String> getOutputColumnNames() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+    return visitor.visitUnion(this, context);
+  }
+
+  @Override
+  protected void serializeAttributes(ByteBuffer byteBuffer) {
+    PlanNodeType.TABLE_UNION_NODE.serialize(byteBuffer);
+    ReadWriteIOUtils.write(getOutputSymbols().size(), byteBuffer);
+    getOutputSymbols().forEach(symbol -> Symbol.serialize(symbol, byteBuffer));
+  }
+
+  @Override
+  protected void serializeAttributes(DataOutputStream stream) throws 
IOException {
+    PlanNodeType.TABLE_UNION_NODE.serialize(stream);
+    ReadWriteIOUtils.write(getOutputSymbols().size(), stream);
+    for (Symbol symbol : getOutputSymbols()) {
+      Symbol.serialize(symbol, stream);
+    }
+  }
+
+  public static UnionNode deserialize(ByteBuffer byteBuffer) {
+    int size = ReadWriteIOUtils.readInt(byteBuffer);
+    List<Symbol> outputs = new ArrayList<>(size);
+    while (size-- > 0) {
+      outputs.add(Symbol.deserialize(byteBuffer));
+    }
+    PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer);
+    return new UnionNode(planNodeId, ImmutableListMultimap.of(), outputs);
+  }
+
+  @Override
+  public PlanNode replaceChildren(List<PlanNode> newChildren) {
+    return new UnionNode(getPlanNodeId(), newChildren, getSymbolMapping(), 
getOutputSymbols());
+  }
+}
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 1112ffd978b..3af300db979 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
@@ -62,6 +62,8 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Pr
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTableFunctionProcessorSourceColumns;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTableScanColumns;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTopKColumns;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneUnionColumns;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneUnionSourceColumns;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneWindowColumns;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushLimitThroughOffset;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushLimitThroughProject;
@@ -132,7 +134,9 @@ public class LogicalOptimizeFactory {
             new PruneWindowColumns(),
             new PruneJoinColumns(),
             new PruneJoinChildrenColumns(),
-            new PrunePatternRecognitionSourceColumns());
+            new PrunePatternRecognitionSourceColumns(),
+            new PruneUnionColumns(),
+            new PruneUnionSourceColumns());
     IterativeOptimizer columnPruningOptimizer =
         new IterativeOptimizer(plannerContext, ruleStats, columnPruningRules);
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
index 8585eb4ebcb..fab09a2e3f5 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
@@ -61,6 +61,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SemiJoinNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ComparisonExpression;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Expression;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.FunctionCall;
@@ -1155,6 +1156,32 @@ public class PushPredicateIntoTableScan implements 
PlanOptimizer {
       return node;
     }
 
+    @Override
+    public PlanNode visitUnion(UnionNode node, RewriteContext context) {
+      boolean modified = false;
+      ImmutableList.Builder<PlanNode> builder = ImmutableList.builder();
+      for (int i = 0; i < node.getChildren().size(); i++) {
+        Expression sourcePredicate =
+            inlineSymbols(node.sourceSymbolMap(i), context.inheritedPredicate);
+        PlanNode child = node.getChildren().get(i);
+        PlanNode rewritten = child.accept(this, new 
RewriteContext(sourcePredicate));
+        if (rewritten != child) {
+          modified = true;
+        }
+        builder.add(rewritten);
+      }
+
+      if (modified) {
+        return new UnionNode(
+            node.getPlanNodeId(),
+            builder.build(),
+            node.getSymbolMapping(),
+            node.getOutputSymbols());
+      }
+
+      return node;
+    }
+
     private DataPartition fetchDataPartitionByDevices(
         final String
             database, // for tree view, database should be the real tree db 
name with `root.` prefix
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
index dbe3735bfca..1d64e28f8e3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
@@ -33,6 +33,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNod
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 
 import com.google.common.collect.ImmutableList;
@@ -144,6 +145,11 @@ public class TransformAggregationToStreamable implements 
PlanOptimizer {
       return node.getChild().accept(this, context);
     }
 
+    @Override
+    public List<Symbol> visitUnion(UnionNode node, GroupContext context) {
+      return ImmutableList.of();
+    }
+
     @Override
     public List<Symbol> visitSort(SortNode node, GroupContext context) {
       return getMatchedPrefixSymbols(context, node.getOrderingScheme());
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
index 38d3d287853..3eb338a650c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
@@ -34,6 +34,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.GroupNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.InformationSchemaTableScanNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.StreamSortNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 
 import java.util.Map;
 
@@ -154,6 +155,12 @@ public class TransformSortToStreamSort implements 
PlanOptimizer {
       context.setCanTransform(false);
       return visitTableScan(node, context);
     }
+
+    @Override
+    public PlanNode visitUnion(UnionNode node, Context context) {
+      context.setCanTransform(false);
+      return visitMultiChildProcess(node, context);
+    }
   }
 
   /**
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
index f94c78a295f..96ab8434475 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
@@ -57,6 +57,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctio
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Expression;
@@ -64,13 +65,17 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.NullLiteral;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SymbolReference;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 
 import java.util.AbstractMap.SimpleEntry;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -972,6 +977,55 @@ public class UnaliasSymbolReferences implements 
PlanOptimizer {
 
       return new PlanAndMappings(rewrittenPatternRecognition, mapping);
     }
+
+    @Override
+    public PlanAndMappings visitUnion(UnionNode node, UnaliasContext context) {
+      List<PlanAndMappings> rewrittenSources =
+          node.getChildren().stream()
+              .map(source -> source.accept(this, context))
+              .collect(toImmutableList());
+
+      List<SymbolMapper> inputMappers =
+          rewrittenSources.stream()
+              .map(source -> symbolMapper(new HashMap<>(source.getMappings())))
+              .collect(toImmutableList());
+
+      Map<Symbol, Symbol> mapping = new 
HashMap<>(context.getCorrelationMapping());
+      SymbolMapper outputMapper = symbolMapper(mapping);
+
+      ListMultimap<Symbol, Symbol> newOutputToInputs =
+          rewriteOutputToInputsMap(node.getSymbolMapping(), outputMapper, 
inputMappers);
+      List<Symbol> newOutputs = 
outputMapper.mapAndDistinct(node.getOutputSymbols());
+
+      return new PlanAndMappings(
+          new UnionNode(
+              node.getPlanNodeId(),
+              
rewrittenSources.stream().map(PlanAndMappings::getRoot).collect(toImmutableList()),
+              newOutputToInputs,
+              newOutputs),
+          mapping);
+    }
+
+    private ListMultimap<Symbol, Symbol> rewriteOutputToInputsMap(
+        ListMultimap<Symbol, Symbol> oldMapping,
+        SymbolMapper outputMapper,
+        List<SymbolMapper> inputMappers) {
+      ImmutableListMultimap.Builder<Symbol, Symbol> newMappingBuilder =
+          ImmutableListMultimap.builder();
+      Set<Symbol> addedSymbols = new HashSet<>();
+      for (Map.Entry<Symbol, Collection<Symbol>> entry : 
oldMapping.asMap().entrySet()) {
+        Symbol rewrittenOutput = outputMapper.map(entry.getKey());
+        if (addedSymbols.add(rewrittenOutput)) {
+          List<Symbol> inputs = ImmutableList.copyOf(entry.getValue());
+          ImmutableList.Builder<Symbol> rewrittenInputs = 
ImmutableList.builder();
+          for (int i = 0; i < inputs.size(); i++) {
+            rewrittenInputs.add(inputMappers.get(i).map(inputs.get(i)));
+          }
+          newMappingBuilder.putAll(rewrittenOutput, rewrittenInputs.build());
+        }
+      }
+      return newMappingBuilder.build();
+    }
   }
 
   private static class UnaliasContext {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
new file mode 100644
index 00000000000..de7ac3327fb
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
@@ -0,0 +1,108 @@
+/*
+ * 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.type;
+
+import org.apache.tsfile.read.common.type.Type;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.tsfile.read.common.type.BinaryType.TEXT;
+import static org.apache.tsfile.read.common.type.BlobType.BLOB;
+import static org.apache.tsfile.read.common.type.BooleanType.BOOLEAN;
+import static org.apache.tsfile.read.common.type.DateType.DATE;
+import static org.apache.tsfile.read.common.type.DoubleType.DOUBLE;
+import static org.apache.tsfile.read.common.type.FloatType.FLOAT;
+import static org.apache.tsfile.read.common.type.IntType.INT32;
+import static org.apache.tsfile.read.common.type.LongType.INT64;
+import static org.apache.tsfile.read.common.type.StringType.STRING;
+import static org.apache.tsfile.read.common.type.TimestampType.TIMESTAMP;
+import static org.apache.tsfile.read.common.type.UnknownType.UNKNOWN;
+
+public class CompatibleResolver {
+
+  private static final Map<Type, Map<Type, Type>> CONDITION_MAP = new 
HashMap<>();
+
+  static {
+    addCondition(INT32, INT32, INT32);
+    addCondition(INT32, INT64, INT64);
+    addCondition(INT32, FLOAT, FLOAT);
+    addCondition(INT32, DOUBLE, DOUBLE);
+    addCondition(INT32, UNKNOWN, INT32);
+
+    addCondition(INT64, INT32, INT64);
+    addCondition(INT64, INT64, INT64);
+    addCondition(INT64, FLOAT, FLOAT);
+    addCondition(INT64, DOUBLE, DOUBLE);
+    addCondition(INT64, TIMESTAMP, TIMESTAMP);
+    addCondition(INT64, UNKNOWN, INT64);
+
+    addCondition(FLOAT, INT32, FLOAT);
+    addCondition(FLOAT, INT64, FLOAT);
+    addCondition(FLOAT, FLOAT, FLOAT);
+    addCondition(FLOAT, DOUBLE, DOUBLE);
+    addCondition(FLOAT, UNKNOWN, FLOAT);
+
+    addCondition(DOUBLE, INT32, DOUBLE);
+    addCondition(DOUBLE, INT64, DOUBLE);
+    addCondition(DOUBLE, FLOAT, DOUBLE);
+    addCondition(DOUBLE, DOUBLE, DOUBLE);
+    addCondition(DOUBLE, UNKNOWN, DOUBLE);
+
+    addCondition(DATE, DATE, DATE);
+    addCondition(DATE, UNKNOWN, DATE);
+
+    addCondition(TIMESTAMP, TIMESTAMP, TIMESTAMP);
+    addCondition(TIMESTAMP, INT64, TIMESTAMP);
+    addCondition(TIMESTAMP, UNKNOWN, TIMESTAMP);
+
+    addCondition(BOOLEAN, BOOLEAN, BOOLEAN);
+    addCondition(BOOLEAN, UNKNOWN, BOOLEAN);
+
+    addCondition(TEXT, TEXT, TEXT);
+    addCondition(TEXT, STRING, STRING);
+    addCondition(TEXT, UNKNOWN, TEXT);
+
+    addCondition(STRING, STRING, STRING);
+    addCondition(STRING, TEXT, STRING);
+    addCondition(STRING, UNKNOWN, STRING);
+
+    addCondition(BLOB, BLOB, BLOB);
+    addCondition(BLOB, UNKNOWN, BLOB);
+
+    addCondition(UNKNOWN, INT32, INT32);
+    addCondition(UNKNOWN, INT64, INT64);
+    addCondition(UNKNOWN, FLOAT, FLOAT);
+    addCondition(UNKNOWN, DOUBLE, DOUBLE);
+    addCondition(UNKNOWN, DATE, DATE);
+    addCondition(UNKNOWN, TIMESTAMP, TIMESTAMP);
+  }
+
+  private static void addCondition(Type condition1, Type condition2, Type 
result) {
+    CONDITION_MAP.computeIfAbsent(condition1, k -> new 
HashMap<>()).put(condition2, result);
+  }
+
+  public static Optional<Type> getCommonSuperType(Type type1, Type type2) {
+    return Optional.ofNullable(
+        CONDITION_MAP.getOrDefault(type1, 
Collections.emptyMap()).getOrDefault(type2, null));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
new file mode 100644
index 00000000000..f0fae2654b7
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.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.tableScan;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+
+public class UnionTest {
+  @Test
+  public void simpleTest() {
+    PlanTester planTester = new PlanTester();
+
+    assertPlan(
+        planTester.createPlan("(select * from table2) union all (select * from 
table3)"),
+        output(union(tableScan("testdb.table2"), tableScan("testdb.table3"))));
+
+    // use Aggregation to process distinct
+    assertPlan(
+        planTester.createPlan("(select * from table2) union (select * from 
table3)"),
+        output(aggregation(union(tableScan("testdb.table2"), 
tableScan("testdb.table3")))));
+
+    // use CAST if types of according columns is not compatible
+    // s1 is INT64, s3 is DOUBLE
+    assertPlan(
+        planTester.createPlan("(select s1, s3 from table2) union (select s1, 
s1 from table3)"),
+        output(
+            aggregation(union(tableScan("testdb.table2"), 
project(tableScan("testdb.table3"))))));
+  }
+
+  @Test
+  public void optimizerTest() {
+    PlanTester planTester = new PlanTester();
+
+    // The predicate will be push down into TableScanNode
+    assertPlan(
+        planTester.createPlan(
+            "select * from ((select * from table2) union all (select * from 
table3)) where s1 > 1"),
+        output(union(tableScan("testdb.table2"), tableScan("testdb.table3"))));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
index d7ca50b1404..9146f46eeca 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
@@ -53,6 +53,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ComparisonExpression;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DataType;
@@ -768,6 +769,10 @@ public final class PlanMatchPattern {
     return node(ExchangeNode.class).with(new ExchangeNodeMatcher());
   }
 
+  public static PlanMatchPattern union(PlanMatchPattern... sources) {
+    return node(UnionNode.class, sources);
+  }
+
   public static PlanMatchPattern enforceSingleRow(PlanMatchPattern source) {
     return node(EnforceSingleRowNode.class, source);
   }

Reply via email to