From 2aac4ae43a0aef96703d8b8b7f0cfbcd53d9a116 Mon Sep 17 00:00:00 2001
From: Weihao Li <18110526956@163.com>
Date: Mon, 1 Jun 2026 17:25:10 +0800
Subject: [PATCH 1/3] fix
Signed-off-by: Weihao Li <18110526956@163.com>
---
.../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 8df10d5d8ec1d..dbb2008f1364c 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.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;
/**
* Optimization phase: Distributed plan planning.
@@ -42,6 +49,8 @@
* SortNode can be eliminated.
*
When order by all IDColumns and time, the SortNode can be eliminated.
* When StreamSortIndex==OrderBy size()-1, remove this StreamSortNode
+ * 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 PlanNode visitPlan(PlanNode node, Context context) {
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 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 projectOutputs = project.getOutputSymbols();
+ Set 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 PlanNode visitSort(SortNode node, Context context) {
&& 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 @@ private static class Context {
private String timeColumnName = null;
+ private boolean sortEliminated = false;
+
Context() {}
public void addDeviceEntrySize(int deviceEntrySize) {
@@ -177,5 +223,13 @@ public String getTimeColumnName() {
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 40797da29a90f..f7b393b724a27 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.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.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.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 void assertTopKNoFilter(
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")));
+ }
}
From 2dafe7f39b82f4a788550a34fcf9dbdc3684bd45 Mon Sep 17 00:00:00 2001
From: Weihao Li <18110526956@163.com>
Date: Mon, 1 Jun 2026 17:30:37 +0800
Subject: [PATCH 2/3] fix
Signed-off-by: Weihao Li <18110526956@163.com>
---
.../planner/optimizations/SortElimination.java | 14 +++-----------
1 file changed, 3 insertions(+), 11 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 dbb2008f1364c..b5ed662cdae42 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
@@ -121,7 +121,9 @@ public PlanNode visitSort(SortNode node, Context context) {
&& orderingScheme.getOrderBy().get(0).getName().equals(context.getTimeColumnName())) {
return child;
}
- return node.replaceChildren(Collections.singletonList(child));
+ return context.canEliminateSort() && node.isOrderByAllIdsAndTime()
+ ? child
+ : node.replaceChildren(Collections.singletonList(child));
}
@Override
@@ -196,8 +198,6 @@ private static class Context {
private String timeColumnName = null;
- private boolean sortEliminated = false;
-
Context() {}
public void addDeviceEntrySize(int deviceEntrySize) {
@@ -223,13 +223,5 @@ public String getTimeColumnName() {
public void setTimeColumnName(String timeColumnName) {
this.timeColumnName = timeColumnName;
}
-
- public boolean isSortEliminated() {
- return sortEliminated;
- }
-
- public void markSortEliminated() {
- this.sortEliminated = true;
- }
}
}
From 9ea7439477c0946889fa4574d8c90a01730d2b7a Mon Sep 17 00:00:00 2001
From: Weihao Li <18110526956@163.com>
Date: Tue, 2 Jun 2026 11:58:14 +0800
Subject: [PATCH 3/3] fix CI
Signed-off-by: Weihao Li <18110526956@163.com>
---
.../iterative/rule/PruneTableScanColumns.java | 54 +++++++++++++++++--
1 file changed, 51 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 f0969147fa587..bf02d62f546e8 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.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 static Optional pruneColumns(TableScanNode node, Set re
.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 static Optional pruneColumns(TableScanNode node, Set re
treeDeviceViewScanNode.getTreeDBName(),
treeDeviceViewScanNode.getMeasurementColumnNameMap()));
} else {
- return Optional.of(
+ DeviceTableScanNode prunedNode =
new DeviceTableScanNode(
deviceTableScanNode.getPlanNodeId(),
deviceTableScanNode.getQualifiedObjectName(),
@@ -126,7 +172,9 @@ public static Optional pruneColumns(TableScanNode node, Set re
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