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 f706aacccaf Fix multiple bugs in window function
f706aacccaf is described below

commit f706aacccafa8c47877d9397a0db37b814defd79
Author: Zhihao Shen <[email protected]>
AuthorDate: Fri Jun 20 22:03:54 2025 +0800

    Fix multiple bugs in window function
---
 .../relational/it/db/it/IoTDBWindowFunctionIT.java | 170 ++++++++++++++++++++-
 .../process/window/TableWindowOperator.java        |   1 -
 .../window/partition/PartitionExecutor.java        |   6 +-
 .../process/window/partition/frame/RangeFrame.java |   6 +-
 .../process/window/utils/RowComparator.java        |  20 +++
 .../relational/analyzer/ExpressionAnalyzer.java    |   5 +-
 .../plan/relational/planner/QueryPlanner.java      |  42 +++--
 .../distribute/TableDistributedPlanGenerator.java  |  11 ++
 .../planner/optimizations/SortElimination.java     |  60 ++++++--
 9 files changed, 285 insertions(+), 36 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowFunctionIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowFunctionIT.java
index 2f5b04a11f5..1be98fa7bf4 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowFunctionIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowFunctionIT.java
@@ -41,7 +41,7 @@ import static org.junit.Assert.fail;
 @Category({TableLocalStandaloneIT.class, TableClusterIT.class})
 public class IoTDBWindowFunctionIT {
   private static final String DATABASE_NAME = "test";
-  private static final String[] sqls =
+  private static final String[] sqlsWithoutNulls =
       new String[] {
         "CREATE DATABASE " + DATABASE_NAME,
         "USE " + DATABASE_NAME,
@@ -55,11 +55,31 @@ public class IoTDBWindowFunctionIT {
         "FLUSH",
         "CLEAR ATTRIBUTE CACHE",
       };
+  private static final String[] sqlsWithNulls =
+      new String[] {
+        "create table demo2 (device string tag, value double field)",
+        "insert into demo2 values (2021-01-01T09:04:00, 'd1', null)",
+        "insert into demo2 values (2021-01-01T09:05:00, 'd1', 3)",
+        "insert into demo2 values (2021-01-01T09:07:00, 'd1', 5)",
+        "insert into demo2 values (2021-01-01T09:09:00, 'd1', 3)",
+        "insert into demo2 values (2021-01-01T09:10:00, 'd1', 1)",
+        "insert into demo2 values (2021-01-01T09:06:00, 'd2', null)",
+        "insert into demo2 values (2021-01-01T09:08:00, 'd2', 2)",
+        "insert into demo2 values (2021-01-01T09:15:00, 'd2', 4)",
+        "insert into demo2 values (2021-01-01T09:20:00, null, null)",
+        "insert into demo2 values (2021-01-01T09:21:00, null, 1)",
+        "insert into demo2 values (2021-01-01T09:22:00, null, 2)",
+        "FLUSH",
+        "CLEAR ATTRIBUTE CACHE",
+      };
 
   private static void insertData() {
     try (Connection connection = EnvFactory.getEnv().getTableConnection();
         Statement statement = connection.createStatement()) {
-      for (String sql : sqls) {
+      for (String sql : sqlsWithoutNulls) {
+        statement.execute(sql);
+      }
+      for (String sql : sqlsWithNulls) {
         statement.execute(sql);
       }
     } catch (Exception e) {
@@ -78,6 +98,25 @@ public class IoTDBWindowFunctionIT {
     EnvFactory.getEnv().cleanClusterEnvironment();
   }
 
+  @Test
+  public void testEmptyOver() {
+    String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
+    String[] retArray =
+        new String[] {
+          "2021-01-01T09:05:00.000Z,d1,3.0,6,",
+          "2021-01-01T09:07:00.000Z,d1,5.0,6,",
+          "2021-01-01T09:09:00.000Z,d1,3.0,6,",
+          "2021-01-01T09:10:00.000Z,d1,1.0,6,",
+          "2021-01-01T09:08:00.000Z,d2,2.0,6,",
+          "2021-01-01T09:15:00.000Z,d2,4.0,6,",
+        };
+    tableResultSetEqualTest(
+        "SELECT *, count(value) OVER () AS cnt FROM demo ORDER BY device",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
   @Test
   public void testPartitionBy() {
     String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
@@ -97,8 +136,75 @@ public class IoTDBWindowFunctionIT {
         DATABASE_NAME);
   }
 
+  @Test
+  public void testPartitionByWithNulls() {
+    String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
+    String[] retArray =
+        new String[] {
+          "2021-01-01T09:04:00.000Z,d1,null,4,",
+          "2021-01-01T09:05:00.000Z,d1,3.0,4,",
+          "2021-01-01T09:07:00.000Z,d1,5.0,4,",
+          "2021-01-01T09:09:00.000Z,d1,3.0,4,",
+          "2021-01-01T09:10:00.000Z,d1,1.0,4,",
+          "2021-01-01T09:06:00.000Z,d2,null,2,",
+          "2021-01-01T09:08:00.000Z,d2,2.0,2,",
+          "2021-01-01T09:15:00.000Z,d2,4.0,2,",
+          "2021-01-01T09:20:00.000Z,null,null,2,",
+          "2021-01-01T09:21:00.000Z,null,1.0,2,",
+          "2021-01-01T09:22:00.000Z,null,2.0,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT *, count(value) OVER (PARTITION BY device) AS cnt FROM demo2 
ORDER BY device",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
   @Test
   public void testOrderBy() {
+    String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
+    String[] retArray =
+        new String[] {
+          "2021-01-01T09:10:00.000Z,d1,1.0,1,",
+          "2021-01-01T09:08:00.000Z,d2,2.0,2,",
+          "2021-01-01T09:05:00.000Z,d1,3.0,4,",
+          "2021-01-01T09:09:00.000Z,d1,3.0,4,",
+          "2021-01-01T09:15:00.000Z,d2,4.0,5,",
+          "2021-01-01T09:07:00.000Z,d1,5.0,6,",
+        };
+    tableResultSetEqualTest(
+        "SELECT *, count(value) OVER (ORDER BY value) AS cnt FROM demo",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
+  @Test
+  public void testOrderByWithNulls() {
+    String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
+    String[] retArray =
+        new String[] {
+          "2021-01-01T09:10:00.000Z,d1,1.0,2,",
+          "2021-01-01T09:21:00.000Z,null,1.0,2,",
+          "2021-01-01T09:08:00.000Z,d2,2.0,4,",
+          "2021-01-01T09:22:00.000Z,null,2.0,4,",
+          "2021-01-01T09:05:00.000Z,d1,3.0,6,",
+          "2021-01-01T09:09:00.000Z,d1,3.0,6,",
+          "2021-01-01T09:15:00.000Z,d2,4.0,7,",
+          "2021-01-01T09:07:00.000Z,d1,5.0,8,",
+          "2021-01-01T09:04:00.000Z,d1,null,8,",
+          "2021-01-01T09:06:00.000Z,d2,null,8,",
+          "2021-01-01T09:20:00.000Z,null,null,8,",
+        };
+    tableResultSetEqualTest(
+        "SELECT *, count(value) OVER (ORDER BY value) AS cnt FROM demo2 ORDER 
BY value, device",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
+  @Test
+  public void testPartitionByAndOrderBy() {
     String[] expectedHeader = new String[] {"time", "device", "value", "rnk"};
     String[] retArray =
         new String[] {
@@ -116,6 +222,30 @@ public class IoTDBWindowFunctionIT {
         DATABASE_NAME);
   }
 
+  @Test
+  public void testPartitionByAndOrderByWithNulls() {
+    String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
+    String[] retArray =
+        new String[] {
+          "2021-01-01T09:10:00.000Z,d1,1.0,1,",
+          "2021-01-01T09:05:00.000Z,d1,3.0,3,",
+          "2021-01-01T09:09:00.000Z,d1,3.0,3,",
+          "2021-01-01T09:07:00.000Z,d1,5.0,4,",
+          "2021-01-01T09:04:00.000Z,d1,null,4,",
+          "2021-01-01T09:08:00.000Z,d2,2.0,1,",
+          "2021-01-01T09:15:00.000Z,d2,4.0,2,",
+          "2021-01-01T09:06:00.000Z,d2,null,2,",
+          "2021-01-01T09:21:00.000Z,null,1.0,1,",
+          "2021-01-01T09:22:00.000Z,null,2.0,2,",
+          "2021-01-01T09:20:00.000Z,null,null,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY value) AS 
cnt FROM demo2 ORDER BY device",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
   @Test
   public void testRowsFraming() {
     String[] expectedHeader = new String[] {"time", "device", "value", "cnt"};
@@ -410,4 +540,40 @@ public class IoTDBWindowFunctionIT {
         retArray,
         DATABASE_NAME);
   }
+
+  @Test
+  public void testNegativeRowsFrameOffset() {
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time ROWS 
-1 PRECEDING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time ROWS 
BETWEEN -2 PRECEDING AND -1 FOLLOWING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time ROWS 
BETWEEN 1 PRECEDING AND -1 FOLLOWING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+  }
+
+  @Test
+  public void testNegativeGroupsFrameOffset() {
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time GROUPS 
-1 PRECEDING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time GROUPS 
BETWEEN -2 PRECEDING AND -1 FOLLOWING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "SELECT *, count(value) OVER (PARTITION BY device ORDER BY time GROUPS 
BETWEEN 1 PRECEDING AND -1 FOLLOWING) AS cnt FROM demo",
+        "Window frame offset value must not be negative or null",
+        DATABASE_NAME);
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/TableWindowOperator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/TableWindowOperator.java
index cb7fb15d470..e1737ffdbcd 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/TableWindowOperator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/TableWindowOperator.java
@@ -294,7 +294,6 @@ public class TableWindowOperator implements ProcessOperator 
{
         partitionExecutors.addLast(partitionExecutor);
 
         partitionStartInCurrentBlock = partitionEndInCurrentBlock;
-        partitionEndInCurrentBlock = partitionStartInCurrentBlock + 1;
       } else {
         // Last partition of TsBlock
         // The beginning of next TsBlock may have rows in this partition
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/PartitionExecutor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/PartitionExecutor.java
index bf136c12391..9075592a7e1 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/PartitionExecutor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/PartitionExecutor.java
@@ -147,7 +147,11 @@ public final class PartitionExecutor {
     for (int i = 0; i < outputChannels.size(); i++) {
       Column column = tsBlock.getColumn(outputChannels.get(i));
       ColumnBuilder columnBuilder = builder.getColumnBuilder(i);
-      columnBuilder.write(column, offsetInTsBlock);
+      if (column.isNull(offsetInTsBlock)) {
+        columnBuilder.appendNull();
+      } else {
+        columnBuilder.write(column, offsetInTsBlock);
+      }
       channel++;
     }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/frame/RangeFrame.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/frame/RangeFrame.java
index b4b4615be15..64981fb6232 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/frame/RangeFrame.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/partition/frame/RangeFrame.java
@@ -40,6 +40,7 @@ public class RangeFrame implements Frame {
   private final FrameInfo frameInfo;
   private boolean noOrderBy = false;
 
+  private List<ColumnList> allSortedColumns;
   private ColumnList column;
   private TSDataType dataType;
 
@@ -64,7 +65,7 @@ public class RangeFrame implements Frame {
     }
 
     // Only one sort key is allowed in range frame
-    checkArgument(sortedColumns.size() == 1);
+    this.allSortedColumns = sortedColumns;
     this.column = sortedColumns.get(0);
     this.dataType = column.getDataType();
     this.peerGroupComparator = comparator;
@@ -87,7 +88,8 @@ public class RangeFrame implements Frame {
         || frameInfo.getStartType() == UNBOUNDED_PRECEDING
             && frameInfo.getEndType() == CURRENT_ROW) {
       if (currentPosition == 0
-          || !peerGroupComparator.equal(column, currentPosition - 1, 
currentPosition)) {
+          || !peerGroupComparator.equalColumnLists(
+              allSortedColumns, currentPosition - 1, currentPosition)) {
         // New peer group
         int frameStart = frameInfo.getStartType() == CURRENT_ROW ? 
peerGroupStart : 0;
         int frameEnd = frameInfo.getEndType() == CURRENT_ROW ? peerGroupEnd - 
1 : partitionSize - 1;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/utils/RowComparator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/utils/RowComparator.java
index cd0c2ffdcff..ac6f2258809 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/utils/RowComparator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/window/utils/RowComparator.java
@@ -50,6 +50,14 @@ public class RowComparator {
   }
 
   private boolean equal(Column column, TSDataType dataType, int offset1, int 
offset2) {
+    if (offset1 == offset2) {
+      return true;
+    }
+
+    if (column.isNull(offset1) || column.isNull(offset2)) {
+      return column.isNull(offset1) && column.isNull(offset2);
+    }
+
     switch (dataType) {
       case BOOLEAN:
         boolean bool1 = column.getBoolean(offset1);
@@ -121,6 +129,14 @@ public class RowComparator {
   }
 
   private boolean equal(ColumnList column, TSDataType dataType, int offset1, 
int offset2) {
+    if (offset1 == offset2) {
+      return true;
+    }
+
+    if (column.isNull(offset1) || column.isNull(offset2)) {
+      return column.isNull(offset1) && column.isNull(offset2);
+    }
+
     switch (dataType) {
       case BOOLEAN:
         boolean bool1 = column.getBoolean(offset1);
@@ -181,6 +197,10 @@ public class RowComparator {
       Column column1 = columns1.get(i);
       Column column2 = columns2.get(i);
 
+      if (column1.isNull(offset1) || column2.isNull(offset2)) {
+        return column1.isNull(offset1) && column2.isNull(offset2);
+      }
+
       switch (dataType) {
         case BOOLEAN:
           boolean bool1 = column1.getBoolean(offset1);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/ExpressionAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/ExpressionAnalyzer.java
index 9032929d2c7..fad9bc32d2f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/ExpressionAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/ExpressionAnalyzer.java
@@ -1778,8 +1778,9 @@ public class ExpressionAnalyzer {
                           "Window frame of type RANGE PRECEDING or FOLLOWING 
requires ORDER BY"));
       if (orderBy.getSortItems().size() != 1) {
         throw new SemanticException(
-            "Window frame of type RANGE PRECEDING or FOLLOWING requires single 
sort item in ORDER BY (actual: %s)",
-            orderBy.getSortItems().size());
+            String.format(
+                "Window frame of type RANGE PRECEDING or FOLLOWING requires 
single sort item in ORDER BY (actual: %s)",
+                orderBy.getSortItems().size()));
       }
       Expression sortKey = 
Iterables.getOnlyElement(orderBy.getSortItems()).getSortKey();
       Type sortKeyType;
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 4df3d4f6562..cb5085d7aa2 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
@@ -394,12 +394,12 @@ public class QueryPlanner {
             window.getFrame().get().getEnd().flatMap(FrameBound::getValue);
 
         // process frame start
-        FrameOffsetPlanAndSymbol plan = planFrameOffset(subPlan, 
startValue.map(coercions::get));
+        FrameOffsetPlanAndSymbol plan = planFrameOffset(subPlan, startValue, 
coercions);
         subPlan = plan.getSubPlan();
         frameStart = plan.getFrameOffsetSymbol();
 
         // process frame end
-        plan = planFrameOffset(subPlan, endValue.map(coercions::get));
+        plan = planFrameOffset(subPlan, endValue, coercions);
         subPlan = plan.getSubPlan();
         frameEnd = plan.getFrameOffsetSymbol();
       } else if (window.getFrame().isPresent()) {
@@ -509,22 +509,26 @@ public class QueryPlanner {
                 }
               }
             });
-    GroupNode groupNode =
-        new GroupNode(
-            queryIdAllocator.genPlanNodeId(),
-            subPlan.getRoot(),
-            new OrderingScheme(sortSymbols, sortOrderings),
-            sortKeyOffset);
-    PlanBuilder planBuilder =
-        new PlanBuilder(
-            
subPlan.getTranslations().withAdditionalMappings(mappings.buildOrThrow()), 
groupNode);
+
+    PlanBuilder planBuilder = null;
+    if (!sortSymbols.isEmpty()) {
+      GroupNode groupNode =
+          new GroupNode(
+              queryIdAllocator.genPlanNodeId(),
+              subPlan.getRoot(),
+              new OrderingScheme(sortSymbols, sortOrderings),
+              sortKeyOffset);
+      planBuilder =
+          new PlanBuilder(
+              
subPlan.getTranslations().withAdditionalMappings(mappings.buildOrThrow()), 
groupNode);
+    }
 
     // create window node
     return new PlanBuilder(
         
subPlan.getTranslations().withAdditionalMappings(mappings.buildOrThrow()),
         new WindowNode(
             queryIdAllocator.genPlanNodeId(),
-            planBuilder.getRoot(),
+            planBuilder != null ? planBuilder.getRoot() : subPlan.getRoot(),
             specification,
             functions.buildOrThrow(),
             Optional.empty(),
@@ -570,7 +574,7 @@ public class QueryPlanner {
       return new FrameBoundPlanAndSymbols(subPlan, Optional.empty(), 
Optional.empty());
     }
 
-    // First, append filter to validate offset values. They mustn't be 
negative or null.
+    // Append filter to validate offset values. They mustn't be negative or 
null.
     Symbol offsetSymbol = coercions.get(frameOffset.get());
     Expression zeroOffset = 
zeroOfType(symbolAllocator.getTypes().getTableModelType(offsetSymbol));
     Expression predicate =
@@ -655,12 +659,20 @@ public class QueryPlanner {
   }
 
   private FrameOffsetPlanAndSymbol planFrameOffset(
-      PlanBuilder subPlan, Optional<Symbol> frameOffset) {
+      PlanBuilder subPlan, Optional<Expression> frameOffset, PlanAndMappings 
coercions) {
     if (!frameOffset.isPresent()) {
       return new FrameOffsetPlanAndSymbol(subPlan, Optional.empty());
     }
 
-    Symbol offsetSymbol = frameOffset.get();
+    // Report error if frame offsets are literals and they are negative or null
+    if (frameOffset.get() instanceof LongLiteral) {
+      long frameOffsetValue = ((LongLiteral) 
frameOffset.get()).getParsedValue();
+      if (frameOffsetValue < 0) {
+        throw new SemanticException("Window frame offset value must not be 
negative or null");
+      }
+    }
+
+    Symbol offsetSymbol = frameOffset.map(coercions::get).get();
     Type offsetType = 
symbolAllocator.getTypes().getTableModelType(offsetSymbol);
 
     // Append filter to validate offset values. They mustn't be negative or 
null.
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 31526d7c0e6..7f43fc7446b 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
@@ -562,9 +562,15 @@ public class TableDistributedPlanGenerator
   @Override
   public List<PlanNode> visitPatternRecognition(PatternRecognitionNode node, 
PlanContext context) {
     context.clearExpectedOrderingScheme();
+    if (node.getPartitionBy().isEmpty()) {
+      Optional<OrderingScheme> orderingScheme = node.getOrderingScheme();
+      orderingScheme.ifPresent(scheme -> 
nodeOrderingMap.put(node.getPlanNodeId(), scheme));
+    }
+
     if (node.getChildren().isEmpty()) {
       return Collections.singletonList(node);
     }
+
     boolean canSplitPushDown = (node.getChild() instanceof GroupNode);
     List<PlanNode> childrenNodes = node.getChild().accept(this, context);
     if (childrenNodes.size() == 1) {
@@ -1648,6 +1654,11 @@ public class TableDistributedPlanGenerator
   @Override
   public List<PlanNode> visitWindowFunction(WindowNode node, PlanContext 
context) {
     context.clearExpectedOrderingScheme();
+    if (node.getSpecification().getPartitionBy().isEmpty()) {
+      Optional<OrderingScheme> orderingScheme = 
node.getSpecification().getOrderingScheme();
+      orderingScheme.ifPresent(scheme -> 
nodeOrderingMap.put(node.getPlanNodeId(), scheme));
+    }
+
     if (node.getChildren().isEmpty()) {
       return Collections.singletonList(node);
     }
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 b785637bb1e..e581678a58f 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
@@ -26,9 +26,11 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.node.FillNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.GapFillNode;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.PatternRecognitionNode;
 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.ValueFillNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
 
 import java.util.Collections;
 
@@ -66,14 +68,14 @@ public class SortElimination implements PlanOptimizer {
     public PlanNode visitSort(SortNode node, Context context) {
       Context newContext = new Context();
       PlanNode child = node.getChild().accept(this, newContext);
-      context.setHasSeenFill(newContext.hasSeenFill);
+      context.setCannotEliminateSort(newContext.cannotEliminateSort);
       OrderingScheme orderingScheme = node.getOrderingScheme();
-      if (!context.hasSeenFill()
+      if (context.canEliminateSort()
           && newContext.getTotalDeviceEntrySize() == 1
           && 
orderingScheme.getOrderBy().get(0).getName().equals(context.getTimeColumnName()))
 {
         return child;
       }
-      return !context.hasSeenFill() && node.isOrderByAllIdsAndTime()
+      return context.canEliminateSort() && node.isOrderByAllIdsAndTime()
           ? child
           : node.replaceChildren(Collections.singletonList(child));
     }
@@ -82,8 +84,8 @@ public class SortElimination implements PlanOptimizer {
     public PlanNode visitStreamSort(StreamSortNode node, Context context) {
       Context newContext = new Context();
       PlanNode child = node.getChild().accept(this, newContext);
-      context.setHasSeenFill(newContext.hasSeenFill);
-      return !context.hasSeenFill()
+      context.setCannotEliminateSort(newContext.cannotEliminateSort);
+      return context.canEliminateSort()
               && (node.isOrderByAllIdsAndTime()
                   || node.getStreamCompareKeyEndIndex()
                       == node.getOrderingScheme().getOrderBy().size() - 1)
@@ -104,7 +106,7 @@ public class SortElimination implements PlanOptimizer {
       for (PlanNode child : node.getChildren()) {
         newNode.addChild(child.accept(this, context));
       }
-      context.setHasSeenFill(!(node instanceof ValueFillNode));
+      context.setCannotEliminateSort(!(node instanceof ValueFillNode));
       return newNode;
     }
 
@@ -114,7 +116,36 @@ public class SortElimination implements PlanOptimizer {
       for (PlanNode child : node.getChildren()) {
         newNode.addChild(child.accept(this, context));
       }
-      context.setHasSeenFill(true);
+      context.setCannotEliminateSort(true);
+      return newNode;
+    }
+
+    @Override
+    public PlanNode visitWindowFunction(WindowNode node, Context context) {
+      PlanNode newNode = node.clone();
+      for (PlanNode child : node.getChildren()) {
+        newNode.addChild(child.accept(this, context));
+      }
+
+      // We can continue to eliminate sort when there is only PARTITION BY
+      if (node.getSpecification().getPartitionBy().isEmpty()
+          || node.getSpecification().getOrderingScheme().isPresent()) {
+        context.setCannotEliminateSort(true);
+      }
+      return newNode;
+    }
+
+    @Override
+    public PlanNode visitPatternRecognition(PatternRecognitionNode node, 
Context context) {
+      PlanNode newNode = node.clone();
+      for (PlanNode child : node.getChildren()) {
+        newNode.addChild(child.accept(this, context));
+      }
+
+      // Same as window function
+      if (node.getPartitionBy().isEmpty() || 
node.getOrderingScheme().isPresent()) {
+        context.setCannotEliminateSort(true);
+      }
       return newNode;
     }
   }
@@ -122,8 +153,11 @@ public class SortElimination implements PlanOptimizer {
   private static class Context {
     private int totalDeviceEntrySize = 0;
 
-    // has seen linear fill, previous fill or gapfill
-    private boolean hasSeenFill = false;
+    // There are 3 situations where sort cannot be eliminated
+    // 1. Query plan has linear fill, previous fill or gapfill
+    // 2. Query plan has window function and it has ordering scheme
+    // 3. Query plan has pattern recognition and it has ordering scheme
+    private boolean cannotEliminateSort = false;
 
     private String timeColumnName = null;
 
@@ -137,12 +171,12 @@ public class SortElimination implements PlanOptimizer {
       return totalDeviceEntrySize;
     }
 
-    public boolean hasSeenFill() {
-      return hasSeenFill;
+    public boolean canEliminateSort() {
+      return !cannotEliminateSort;
     }
 
-    public void setHasSeenFill(boolean hasSeenFill) {
-      this.hasSeenFill = hasSeenFill;
+    public void setCannotEliminateSort(boolean cannotEliminateSort) {
+      this.cannotEliminateSort = cannotEliminateSort;
     }
 
     public String getTimeColumnName() {

Reply via email to