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

Wei-hao-Li pushed a commit to branch fixProject
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit c145cf0dd28a064712dfd14551a0a10aba5e6f24
Author: Weihao Li <[email protected]>
AuthorDate: Mon Jun 1 17:25:10 2026 +0800

    fix
    
    Signed-off-by: Weihao Li <[email protected]>
---
 .../planner/optimizations/SortElimination.java     | 60 ++++++++++++++++++++--
 .../plan/relational/analyzer/SortTest.java         | 44 ++++++++++++++++
 2 files changed, 101 insertions(+), 3 deletions(-)

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..dbb2008f136 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();
@@ -75,9 +121,7 @@ public class SortElimination implements PlanOptimizer {
           && 
orderingScheme.getOrderBy().get(0).getName().equals(context.getTimeColumnName()))
 {
         return child;
       }
-      return context.canEliminateSort() && node.isOrderByAllIdsAndTime()
-          ? child
-          : node.replaceChildren(Collections.singletonList(child));
+      return node.replaceChildren(Collections.singletonList(child));
     }
 
     @Override
@@ -152,6 +196,8 @@ public class SortElimination implements PlanOptimizer {
 
     private String timeColumnName = null;
 
+    private boolean sortEliminated = false;
+
     Context() {}
 
     public void addDeviceEntrySize(int deviceEntrySize) {
@@ -177,5 +223,13 @@ public class SortElimination implements PlanOptimizer {
     public void setTimeColumnName(String timeColumnName) {
       this.timeColumnName = timeColumnName;
     }
+
+    public boolean isSortEliminated() {
+      return sortEliminated;
+    }
+
+    public void markSortEliminated() {
+      this.sortEliminated = true;
+    }
   }
 }
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