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

JackieTien97 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 66d0c07bb09 Remove useless ProjectNode after SortElimination (#17806)
66d0c07bb09 is described below

commit 66d0c07bb09082ec71cc77911f46b83e02c773a3
Author: Weihao Li <[email protected]>
AuthorDate: Wed Jun 3 17:33:35 2026 +0800

    Remove useless ProjectNode after SortElimination (#17806)
---
 .../iterative/rule/PruneTableScanColumns.java      | 54 ++++++++++++++++++++--
 .../planner/optimizations/SortElimination.java     | 46 ++++++++++++++++++
 .../plan/relational/analyzer/SortTest.java         | 44 ++++++++++++++++++
 3 files changed, 141 insertions(+), 3 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneTableScanColumns.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneTableScanColumns.java
index f0969147fa5..bf02d62f546 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneTableScanColumns.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneTableScanColumns.java
@@ -28,7 +28,9 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.SymbolsExtractor;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationTableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.InformationSchemaTableScanNode;
+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 java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -91,7 +93,51 @@ public class PruneTableScanColumns extends 
ProjectOffPushDownRule<TableScanNode>
                       .forEach(
                           symbol -> newAssignments.put(symbol, 
node.getAssignments().get(symbol))));
 
-      if (node instanceof TreeDeviceViewScanNode) {
+      if (node instanceof TreeAlignedDeviceViewScanNode) {
+        TreeAlignedDeviceViewScanNode treeDeviceViewScanNode =
+            (TreeAlignedDeviceViewScanNode) deviceTableScanNode;
+        TreeAlignedDeviceViewScanNode prunedNode =
+            new TreeAlignedDeviceViewScanNode(
+                deviceTableScanNode.getPlanNodeId(),
+                deviceTableScanNode.getQualifiedObjectName(),
+                newOutputs,
+                newAssignments,
+                deviceTableScanNode.getDeviceEntries(),
+                deviceTableScanNode.getTagAndAttributeIndexMap(),
+                deviceTableScanNode.getScanOrder(),
+                deviceTableScanNode.getTimePredicate().orElse(null),
+                deviceTableScanNode.getPushDownPredicate(),
+                deviceTableScanNode.getPushDownLimit(),
+                deviceTableScanNode.getPushDownOffset(),
+                deviceTableScanNode.isPushLimitToEachDevice(),
+                deviceTableScanNode.containsNonAlignedDevice(),
+                treeDeviceViewScanNode.getTreeDBName(),
+                treeDeviceViewScanNode.getMeasurementColumnNameMap());
+        
prunedNode.setRegionReplicaSet(deviceTableScanNode.getRegionReplicaSet());
+        return Optional.of(prunedNode);
+      } else if (node instanceof TreeNonAlignedDeviceViewScanNode) {
+        TreeNonAlignedDeviceViewScanNode treeDeviceViewScanNode =
+            (TreeNonAlignedDeviceViewScanNode) deviceTableScanNode;
+        TreeNonAlignedDeviceViewScanNode prunedNode =
+            new TreeNonAlignedDeviceViewScanNode(
+                deviceTableScanNode.getPlanNodeId(),
+                deviceTableScanNode.getQualifiedObjectName(),
+                newOutputs,
+                newAssignments,
+                deviceTableScanNode.getDeviceEntries(),
+                deviceTableScanNode.getTagAndAttributeIndexMap(),
+                deviceTableScanNode.getScanOrder(),
+                deviceTableScanNode.getTimePredicate().orElse(null),
+                deviceTableScanNode.getPushDownPredicate(),
+                deviceTableScanNode.getPushDownLimit(),
+                deviceTableScanNode.getPushDownOffset(),
+                deviceTableScanNode.isPushLimitToEachDevice(),
+                deviceTableScanNode.containsNonAlignedDevice(),
+                treeDeviceViewScanNode.getTreeDBName(),
+                treeDeviceViewScanNode.getMeasurementColumnNameMap());
+        
prunedNode.setRegionReplicaSet(deviceTableScanNode.getRegionReplicaSet());
+        return Optional.of(prunedNode);
+      } else if (node instanceof TreeDeviceViewScanNode) {
         TreeDeviceViewScanNode treeDeviceViewScanNode =
             (TreeDeviceViewScanNode) deviceTableScanNode;
         return Optional.of(
@@ -112,7 +158,7 @@ public class PruneTableScanColumns extends 
ProjectOffPushDownRule<TableScanNode>
                 treeDeviceViewScanNode.getTreeDBName(),
                 treeDeviceViewScanNode.getMeasurementColumnNameMap()));
       } else {
-        return Optional.of(
+        DeviceTableScanNode prunedNode =
             new DeviceTableScanNode(
                 deviceTableScanNode.getPlanNodeId(),
                 deviceTableScanNode.getQualifiedObjectName(),
@@ -126,7 +172,9 @@ public class PruneTableScanColumns extends 
ProjectOffPushDownRule<TableScanNode>
                 deviceTableScanNode.getPushDownLimit(),
                 deviceTableScanNode.getPushDownOffset(),
                 deviceTableScanNode.isPushLimitToEachDevice(),
-                deviceTableScanNode.containsNonAlignedDevice()));
+                deviceTableScanNode.containsNonAlignedDevice());
+        
prunedNode.setRegionReplicaSet(deviceTableScanNode.getRegionReplicaSet());
+        return Optional.of(prunedNode);
       }
     } else if (node instanceof InformationSchemaTableScanNode) {
       // For the convenience of process in execution stage, column-prune for
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/SortElimination.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/SortElimination.java
index 8df10d5d8ec..b5ed662cdae 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/SortElimination.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/SortElimination.java
@@ -25,14 +25,21 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.FillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.GapFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.StreamSortNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValueFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTableScanColumns;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
 
+import com.google.common.collect.ImmutableSet;
+
 import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 
 /**
  * <b>Optimization phase:</b> Distributed plan planning.
@@ -42,6 +49,8 @@ import java.util.Collections;
  *     SortNode can be eliminated.
  * <li>When order by all IDColumns and time, the SortNode can be eliminated.
  * <li>When StreamSortIndex==OrderBy size()-1, remove this StreamSortNode
+ * <li>After SortNode elimination, visitProject will remove redundant identity 
ProjectNodes above
+ *     TableScan and pushes column pruning into the scan.
  */
 public class SortElimination implements PlanOptimizer {
 
@@ -64,6 +73,43 @@ public class SortElimination implements PlanOptimizer {
       return newNode;
     }
 
+    @Override
+    public PlanNode visitProject(ProjectNode node, Context context) {
+      Context newContext = new Context();
+      PlanNode child = node.getChild().accept(this, newContext);
+      context.setCannotEliminateSort(newContext.cannotEliminateSort);
+
+      // Remove useless ProjectNode and prune columns of TableScanNode
+      return eliminateProjectOverTableScan(node, child)
+          .orElseGet(() -> 
node.replaceChildren(Collections.singletonList(child)));
+    }
+
+    private static Optional<PlanNode> eliminateProjectOverTableScan(
+        ProjectNode project, PlanNode child) {
+      if (!(child instanceof DeviceTableScanNode) || !project.isIdentity()) {
+        return Optional.empty();
+      }
+
+      // Notice that SortNode may have been eliminated in 
TableDistributedPlanGenerator
+      DeviceTableScanNode tableScan = (DeviceTableScanNode) child;
+      int projectOutputsSize = project.getOutputSymbols().size();
+      int scanOutputsSize = tableScan.getOutputSymbols().size();
+      if (projectOutputsSize > scanOutputsSize) {
+        return Optional.empty();
+      }
+
+      List<Symbol> projectOutputs = project.getOutputSymbols();
+      Set<Symbol> scanOutputs = 
ImmutableSet.copyOf(tableScan.getOutputSymbols());
+      if (!scanOutputs.containsAll(projectOutputs)) {
+        return Optional.empty();
+      }
+
+      if (projectOutputsSize == scanOutputsSize) {
+        return Optional.of(tableScan);
+      }
+      return PruneTableScanColumns.pruneColumns(tableScan, 
ImmutableSet.copyOf(projectOutputs));
+    }
+
     @Override
     public PlanNode visitSort(SortNode node, Context context) {
       Context newContext = new Context();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SortTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SortTest.java
index 40797da29a9..f7b393b724a 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SortTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SortTest.java
@@ -35,6 +35,7 @@ import 
org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.sink.IdentitySinkNode;
 import org.apache.iotdb.db.queryengine.plan.relational.metadata.Metadata;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.SymbolAllocator;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.TableLogicalPlanner;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.distribute.TableDistributedPlanner;
@@ -42,6 +43,8 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableS
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode;
 import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -58,6 +61,12 @@ import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils
 import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.TEST_MATADATA;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.assertTableScan;
 import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.getChildrenNode;
+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.exchange;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.mergeSort;
+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.statement.component.Ordering.ASC;
 import static 
org.apache.iotdb.db.queryengine.plan.statement.component.Ordering.DESC;
 import static org.junit.Assert.assertEquals;
@@ -767,4 +776,39 @@ public class SortTest {
         expectedPushDownOffset,
         isPushLimitToEachDevice);
   }
+
+  @Test
+  public void singleDeviceOrderByAllIdsAndTimeDescTest() {
+    PlanTester planTester = new PlanTester();
+    planTester.createPlan(
+        "SELECT s1, s2, s3 FROM table1 "
+            + "WHERE time >= 1000 AND time <= 2000 AND tag1='beijing' AND 
tag2='A1'"
+            + "ORDER BY tag1, tag2, tag3, time DESC");
+    assertPlan(
+        planTester.getFragmentPlan(0),
+        output(
+            tableScan(
+                "testdb.table1",
+                ImmutableList.of("s1", "s2", "s3"),
+                ImmutableSet.of("time", "s1", "s2", "s3"))));
+  }
+
+  @Test
+  public void multiDeviceOrderByAllIdsAndTimeDescTest() {
+    PlanTester planTester = new PlanTester();
+    planTester.createPlan(
+        "SELECT s1, s2, s3 FROM table1 "
+            + "WHERE time >= 1000 AND time <= 2000 AND tag1='beijing' "
+            + "ORDER BY tag1, tag2, tag3, time DESC");
+    assertPlan(
+        planTester.getFragmentPlan(0),
+        output(project(mergeSort(exchange(), exchange(), exchange()))));
+    // Device in multi-region
+    assertPlan(
+        planTester.getFragmentPlan(1),
+        tableScan(
+            "testdb.table1",
+            ImmutableList.of("time", "tag1", "tag2", "tag3", "s1", "s2", "s3"),
+            ImmutableSet.of("time", "tag1", "tag2", "tag3", "s1", "s2", 
"s3")));
+  }
 }

Reply via email to