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 ffddfde81ec [IOTDB-17798] Implement table model NEXT fill (#17810)
ffddfde81ec is described below

commit ffddfde81ec152f369cfdbc6fbe47d99ee3d2a7e
Author: DaZuiZui <[email protected]>
AuthorDate: Tue Jun 9 15:48:21 2026 +0800

    [IOTDB-17798] Implement table model NEXT fill (#17810)
---
 .../it/query/recent/IoTDBFillTableIT.java          | 155 +++++++++++
 .../src/main/codegen/templates/nextFill.ftl        | 101 +++++++
 .../operator/process/TableNextFillOperator.java    |  81 ++++++
 .../process/TableNextFillWithGroupOperator.java    | 165 ++++++++++++
 .../operator/process/fill/next/NextFill.java       | 182 +++++++++++++
 .../calc/plan/planner/CommonOperatorUtils.java     | 105 +++++---
 .../calc/plan/planner/TableOperatorGenerator.java  |  44 +++
 .../plan/planner/plan/node/PlanGraphPrinter.java   |  15 ++
 .../plan/relational/analyzer/Analysis.java         |  26 ++
 .../relational/analyzer/StatementAnalyzer.java     |  22 ++
 .../plan/relational/planner/QueryPlanner.java      |  22 ++
 .../planner/iterative/rule/PruneFillColumns.java   |   5 +
 .../PushLimitOffsetIntoTableScan.java              |   7 +
 .../optimizations/UnaliasSymbolReferences.java     |  32 +++
 .../plan/relational/sql/parser/AstBuilder.java     |  38 +++
 .../operator/process/fill/NextFillTest.java        | 299 +++++++++++++++++++++
 .../relational/planner/NextFillNodeSerdeTest.java  | 112 ++++++++
 .../relational/sql/parser/FillStatementTest.java   |  68 +++++
 .../plan/node/CommonPlanNodeDeserializer.java      |   3 +
 .../planner/plan/node/ICoreQueryPlanVisitor.java   |   5 +
 .../plan/planner/plan/node/PlanNodeType.java       |   1 +
 .../{PreviousFillNode.java => NextFillNode.java}   |  27 +-
 .../relational/planner/node/PreviousFillNode.java  |   5 +-
 .../queryengine/plan/relational/sql/ast/Fill.java  |  22 +-
 .../sql/util/CommonQuerySqlFormatter.java          |   3 +-
 .../plan/statement/component/FillPolicy.java       |   1 +
 .../db/relational/grammar/sql/RelationalSql.g4     |   1 +
 27 files changed, 1493 insertions(+), 54 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFillTableIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFillTableIT.java
index edd66bdee2d..18d2d3c3078 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFillTableIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFillTableIT.java
@@ -653,6 +653,142 @@ public class IoTDBFillTableIT {
     }
   }
 
+  @Test
+  public void nextFillTest() {
+    String[] expectedHeader =
+        new String[] {
+          "time", "device_id", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", 
"s9", "s10"
+        };
+    String[] retArray =
+        new String[] {
+          
"1970-01-01T00:00:00.001Z,d1,1,11,1.1,11.1,true,text1,string1,0xcafebabe01,1970-01-01T00:00:00.001Z,2024-10-01,",
+          
"1970-01-01T00:00:00.002Z,d1,2,22,2.2,22.2,false,text3,string3,0xcafebabe03,1970-01-01T00:00:00.003Z,2024-10-03,",
+          
"1970-01-01T00:00:00.003Z,d1,5,55,5.5,55.5,false,text3,string3,0xcafebabe03,1970-01-01T00:00:00.003Z,2024-10-03,",
+          
"1970-01-01T00:00:00.004Z,d1,5,55,5.5,55.5,false,text4,string4,0xcafebabe04,1970-01-01T00:00:00.004Z,2024-10-04,",
+          
"1970-01-01T00:00:00.005Z,d1,5,55,5.5,55.5,false,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+          
"1970-01-01T00:00:00.007Z,d1,7,77,7.7,77.7,true,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+          
"1970-01-01T00:00:00.008Z,d1,null,null,null,null,null,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+        };
+    tableResultSetEqualTest(
+        "select * from table1 FILL METHOD NEXT", expectedHeader, retArray, 
DATABASE_NAME);
+
+    retArray =
+        new String[] {
+          
"1970-01-01T00:00:00.001Z,d1,1,11,1.1,11.1,true,text1,string1,0xcafebabe01,1970-01-01T00:00:00.001Z,2024-10-01,",
+          
"1970-01-01T00:00:00.002Z,d1,2,22,2.2,22.2,false,text3,string3,0xcafebabe03,1970-01-01T00:00:00.003Z,2024-10-03,",
+          
"1970-01-01T00:00:00.003Z,d1,5,55,5.5,55.5,false,text3,string3,0xcafebabe03,1970-01-01T00:00:00.003Z,2024-10-03,",
+          
"1970-01-01T00:00:00.004Z,d1,5,55,5.5,55.5,false,text4,string4,0xcafebabe04,1970-01-01T00:00:00.004Z,2024-10-04,",
+          
"1970-01-01T00:00:00.005Z,d1,5,55,5.5,55.5,false,null,null,null,null,null,",
+          
"1970-01-01T00:00:00.007Z,d1,7,77,7.7,77.7,true,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+          
"1970-01-01T00:00:00.008Z,d1,null,null,null,null,null,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+        };
+    tableResultSetEqualTest(
+        "select * from table1 FILL METHOD NEXT TIME_BOUND 2ms TIME_COLUMN 1",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    retArray =
+        new String[] {
+          
"1970-01-01T00:00:00.001Z,d1,1,11,1.1,11.1,true,text1,string1,0xcafebabe01,1970-01-01T00:00:00.001Z,2024-10-01,",
+          
"1970-01-01T00:00:00.002Z,d1,2,22,2.2,22.2,false,null,null,null,null,null,",
+          
"1970-01-01T00:00:00.003Z,d1,null,null,null,null,null,text3,string3,0xcafebabe03,1970-01-01T00:00:00.003Z,2024-10-03,",
+          
"1970-01-01T00:00:00.004Z,d1,null,null,null,null,null,text4,string4,0xcafebabe04,1970-01-01T00:00:00.004Z,2024-10-04,",
+          
"1970-01-01T00:00:00.005Z,d1,5,55,5.5,55.5,false,null,null,null,null,null,",
+          
"1970-01-01T00:00:00.007Z,d1,7,77,7.7,77.7,true,null,null,null,null,null,",
+          
"1970-01-01T00:00:00.008Z,d1,null,null,null,null,null,text8,string8,0xcafebabe08,1970-01-01T00:00:00.008Z,2024-10-08,",
+        };
+    tableResultSetEqualTest(
+        "select * from table1 FILL METHOD NEXT TIME_BOUND 2ms TIME_COLUMN 11",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    expectedHeader = new String[] {"s1", "s6"};
+    retArray =
+        new String[] {
+          "1,text1,", "2,text3,", "5,text3,", "5,text4,", "5,text8,", 
"7,text8,", "null,text8,",
+        };
+    tableResultSetEqualTest(
+        "select s1,s6 from table1 FILL METHOD NEXT", expectedHeader, retArray, 
DATABASE_NAME);
+
+    expectedHeader = new String[] {"time", "s1", "s6"};
+    retArray =
+        new String[] {
+          "1970-01-01T00:00:00.008Z,null,text8,",
+          "1970-01-01T00:00:00.007Z,7,text8,",
+          "1970-01-01T00:00:00.005Z,5,text8,",
+          "1970-01-01T00:00:00.004Z,5,text4,",
+          "1970-01-01T00:00:00.003Z,5,text3,",
+          "1970-01-01T00:00:00.002Z,2,text3,",
+          "1970-01-01T00:00:00.001Z,1,text1,",
+        };
+    tableResultSetEqualTest(
+        "select time,s1,s6 from table1 FILL METHOD NEXT order by time desc",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,1,text1,",
+          "1970-01-01T00:00:00.002Z,2,text1,",
+          "1970-01-01T00:00:00.003Z,2,text3,",
+          "1970-01-01T00:00:00.004Z,2,text4,",
+          "1970-01-01T00:00:00.005Z,5,text4,",
+          "1970-01-01T00:00:00.007Z,7,text4,",
+          "1970-01-01T00:00:00.008Z,7,text8,",
+        };
+    tableResultSetEqualTest(
+        "select * from (select time,s1,s6 from table1 order by time desc) FILL 
METHOD NEXT order by time",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    expectedHeader = new String[] {"time", "s1"};
+    retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,1,",
+          "1970-01-01T00:00:00.002Z,2,",
+          "1970-01-01T00:00:00.003Z,5,",
+        };
+    tableResultSetEqualTest(
+        "select time,s1 from table1 FILL METHOD NEXT limit 3",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+
+    expectedHeader = new String[] {"time", "city", "device_id", "s1", "s2"};
+    retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,beijing,d1,102,1011,",
+          "1970-01-01T00:00:00.002Z,beijing,d1,102,null,",
+          "1970-01-01T00:00:00.003Z,beijing,d1,103,null,",
+          "1970-01-01T00:00:00.004Z,beijing,d1,104,null,",
+          "1970-01-01T00:00:00.001Z,beijing,d2,101,1022,",
+          "1970-01-01T00:00:00.002Z,beijing,d2,null,1022,",
+          "1970-01-01T00:00:00.003Z,beijing,d2,null,1033,",
+          "1970-01-01T00:00:00.004Z,beijing,d2,null,1044,",
+          "1970-01-01T00:00:00.001Z,shanghai,d1,212,2111,",
+          "1970-01-01T00:00:00.002Z,shanghai,d1,212,null,",
+        };
+    tableResultSetEqualTest(
+        "select time,city,device_id,s1,s2 from table2 FILL METHOD NEXT 
FILL_GROUP 2,3",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+    tableResultSetEqualTest(
+        "select time,city,device_id,s1,s2 from table2 FILL METHOD NEXT 
TIME_COLUMN 1 FILL_GROUP 2,3",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+    tableResultSetEqualTest(
+        "select time,city,device_id,s1,s2 from table2 FILL METHOD NEXT 
TIME_BOUND 2ms TIME_COLUMN 1 FILL_GROUP 2,3",
+        expectedHeader,
+        retArray,
+        DATABASE_NAME);
+  }
+
   @Test
   public void abNormalFillTest() {
 
@@ -705,6 +841,25 @@ public class IoTDBFillTableIT {
             + ": PREVIOUS FILL FILL_GROUP position 3 is not in select list",
         DATABASE_NAME);
 
+    // --------------------------------- NEXT FILL 
---------------------------------
+    tableAssertTestFail(
+        "select s1 from table1 FILL METHOD NEXT TIME_COLUMN 1",
+        TSStatusCode.SEMANTIC_ERROR.getStatusCode()
+            + ": Don't need to specify TIME_COLUMN while either TIME_BOUND or 
FILL_GROUP parameter is not specified",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "select s1 from table1 FILL METHOD NEXT TIME_BOUND 2ms",
+        TSStatusCode.SEMANTIC_ERROR.getStatusCode()
+            + ": Cannot infer TIME_COLUMN for NEXT FILL, there exists no 
column whose type is TIMESTAMP",
+        DATABASE_NAME);
+
+    tableAssertTestFail(
+        "select s1 from table1 FILL METHOD NEXT FILL_GROUP 1",
+        TSStatusCode.SEMANTIC_ERROR.getStatusCode()
+            + ": Cannot infer TIME_COLUMN for NEXT FILL, there exists no 
column whose type is TIMESTAMP",
+        DATABASE_NAME);
+
     // --------------------------------- LINEAR FILL 
---------------------------------
     tableAssertTestFail(
         "select s1 from table1 FILL METHOD LINEAR",
diff --git a/iotdb-core/calc-commons/src/main/codegen/templates/nextFill.ftl 
b/iotdb-core/calc-commons/src/main/codegen/templates/nextFill.ftl
new file mode 100644
index 00000000000..e3353141eed
--- /dev/null
+++ b/iotdb-core/calc-commons/src/main/codegen/templates/nextFill.ftl
@@ -0,0 +1,101 @@
+/*
+* 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.
+*/
+<@pp.dropOutputFile />
+
+<#list allDataTypes.types as type>
+
+  <#assign className = "${type.dataType?cap_first}NextFill">
+  <@pp.changeOutputFile 
name="/org/apache/iotdb/calc/execution/operator/process/fill/next/${className}.java"
 />
+package org.apache.iotdb.calc.execution.operator.process.fill.next;
+
+import org.apache.iotdb.calc.execution.operator.process.fill.IFillFilter;
+import org.apache.tsfile.block.column.Column;
+import org.apache.tsfile.read.common.block.column.${type.column};
+import org.apache.tsfile.read.common.block.column.${type.column}Builder;
+import org.apache.tsfile.read.common.block.column.RunLengthEncodedColumn;
+<#if type.dataType == "Binary">
+import org.apache.tsfile.utils.Binary;
+</#if>
+
+import java.util.Optional;
+
+/*
+* This class is generated using freemarker and the ${.template_name} template.
+*/
+@SuppressWarnings("unused")
+public class ${className} extends NextFill {
+
+  private ${type.dataType} nextValue;
+  private ${type.dataType} nextValueInCurrentColumn;
+
+  public ${className}(IFillFilter filter) {
+    super(filter);
+  }
+
+  @Override
+  void fillValue(Column column, int index, Object array) {
+    ((${type.dataType}[]) array)[index] = 
column.get${type.dataType?cap_first}(index);
+  }
+
+  @Override
+  void fillNextValueInCurrentColumn(Object array, int index) {
+    ((${type.dataType}[]) array)[index] = nextValueInCurrentColumn;
+  }
+
+  @Override
+  Object createValueArray(int size) {
+    return new ${type.dataType}[size];
+  }
+
+  @Override
+  Column createNullValueColumn() {
+    return ${type.column}Builder.NULL_VALUE_BLOCK;
+  }
+
+  @Override
+  Column createRunLengthEncodedFilledValueColumn(int size) {
+    return new RunLengthEncodedColumn(
+        new ${type.column}(1, Optional.empty(), new ${type.dataType}[] 
{nextValueInCurrentColumn}), size);
+  }
+
+  @Override
+  Column createFilledValueColumn(Object array, boolean[] isNull, boolean 
hasNullValue, int size) {
+    if (hasNullValue) {
+      return new ${type.column}(size, Optional.of(isNull), 
(${type.dataType}[]) array);
+    } else {
+      return new ${type.column}(size, Optional.empty(), (${type.dataType}[]) 
array);
+    }
+  }
+
+  @Override
+  void updateNextValue(Column nextValueColumn, int index) {
+    this.nextValue = nextValueColumn.get${type.dataType?cap_first}(index);
+  }
+
+  @Override
+  void updateNextValueInCurrentColumn(Column nextValueColumn, int index) {
+    this.nextValueInCurrentColumn = 
nextValueColumn.get${type.dataType?cap_first}(index);
+  }
+
+  @Override
+  void updateNextValueInCurrentColumn() {
+    this.nextValueInCurrentColumn = this.nextValue;
+  }
+}
+</#list>
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillOperator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillOperator.java
new file mode 100644
index 00000000000..1630336b267
--- /dev/null
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillOperator.java
@@ -0,0 +1,81 @@
+/*
+ * 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.calc.execution.operator.process;
+
+import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
+import org.apache.iotdb.calc.execution.operator.Operator;
+import org.apache.iotdb.calc.execution.operator.process.fill.ILinearFill;
+
+import org.apache.tsfile.block.column.Column;
+import org.apache.tsfile.read.common.block.TsBlock;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class TableNextFillOperator extends AbstractLinearFillOperator {
+
+  // start from 0; -1 means plain NEXT has no helper column.
+  private final int helperColumnIndex;
+
+  private final boolean hasTimeBound;
+
+  public TableNextFillOperator(
+      CommonOperatorContext operatorContext,
+      ILinearFill[] fillArray,
+      Operator child,
+      int helperColumnIndex,
+      boolean hasTimeBound) {
+    super(operatorContext, fillArray, child);
+    checkArgument(
+        !hasTimeBound || helperColumnIndex != -1,
+        "helperColumnIndex should be resolved when timeBound exists");
+    this.helperColumnIndex = helperColumnIndex;
+    this.hasTimeBound = hasTimeBound;
+  }
+
+  @Override
+  protected Column getHelperColumn(TsBlock tsBlock) {
+    return helperColumnIndex == -1 ? tsBlock.getTimeColumn() : 
tsBlock.getColumn(helperColumnIndex);
+  }
+
+  @Override
+  protected Integer getLastRowIndexForNonNullHelperColumn(TsBlock tsBlock) {
+    int size = tsBlock.getPositionCount();
+    if (!hasTimeBound) {
+      return size - 1;
+    }
+    Column helperColumn = getHelperColumn(tsBlock);
+    if (!helperColumn.mayHaveNull()) {
+      return size - 1;
+    } else {
+      int i = size - 1;
+      for (; i >= 0; i--) {
+        if (!helperColumn.isNull(i)) {
+          break;
+        }
+      }
+      return i;
+    }
+  }
+
+  @Override
+  public long ramBytesUsed() {
+    return super.ramBytesUsed() + Integer.BYTES;
+  }
+}
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillWithGroupOperator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillWithGroupOperator.java
new file mode 100644
index 00000000000..71ddc5fb30b
--- /dev/null
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableNextFillWithGroupOperator.java
@@ -0,0 +1,165 @@
+/*
+ * 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.calc.execution.operator.process;
+
+import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
+import org.apache.iotdb.calc.execution.operator.Operator;
+import org.apache.iotdb.calc.execution.operator.process.fill.ILinearFill;
+import org.apache.iotdb.calc.plan.planner.CommonOperatorUtils;
+import org.apache.iotdb.calc.utils.datastructure.SortKey;
+
+import org.apache.tsfile.block.column.Column;
+import org.apache.tsfile.block.column.ColumnBuilder;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.block.TsBlock;
+import org.apache.tsfile.read.common.block.TsBlockBuilder;
+import org.apache.tsfile.read.common.block.column.RunLengthEncodedColumn;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class TableNextFillWithGroupOperator extends TableNextFillOperator {
+
+  private final List<Boolean> noMoreTsBlockForCurrentGroup;
+
+  private final Comparator<SortKey> groupKeyComparator;
+
+  private final TsBlockBuilder resultBuilder;
+
+  private SortKey lastRow = null;
+
+  public TableNextFillWithGroupOperator(
+      CommonOperatorContext operatorContext,
+      ILinearFill[] fillArray,
+      Operator child,
+      int helperColumnIndex,
+      boolean hasTimeBound,
+      Comparator<SortKey> groupKeyComparator,
+      List<TSDataType> dataTypes) {
+    super(operatorContext, fillArray, child, helperColumnIndex, hasTimeBound);
+    this.noMoreTsBlockForCurrentGroup = new ArrayList<>();
+    this.groupKeyComparator = groupKeyComparator;
+    this.resultBuilder = new TsBlockBuilder(dataTypes);
+  }
+
+  @Override
+  // we won't build timeColumn in this method
+  TsBlock append(int length, Column timeColumn, Column[] valueColumns) {
+    for (int i = 0; i < outputColumnCount; i++) {
+      Column column = valueColumns[i];
+      ColumnBuilder builder = resultBuilder.getColumnBuilder(i);
+      for (int rowIndex = 0; rowIndex < length; rowIndex++) {
+        if (column.isNull(rowIndex)) {
+          builder.appendNull();
+        } else {
+          builder.write(column, rowIndex);
+        }
+      }
+    }
+    resultBuilder.declarePositions(length);
+    return null;
+  }
+
+  @Override
+  TsBlock buildFinalResult(TsBlock tempResult) {
+    TsBlock result = null;
+    if (!resultBuilder.isEmpty()) {
+      Column timeColumn =
+          new RunLengthEncodedColumn(
+              CommonOperatorUtils.TIME_COLUMN_TEMPLATE, 
resultBuilder.getPositionCount());
+      result = resultBuilder.build(timeColumn);
+      resultBuilder.reset();
+    }
+    return result;
+  }
+
+  @Override
+  boolean noMoreTsBlockForCurrentGroup() {
+    return noMoreTsBlock || noMoreTsBlockForCurrentGroup.get(0);
+  }
+
+  @Override
+  void resetFill() {
+    boolean isNoMoreTsBlockForCurrentGroup =
+        Boolean.TRUE.equals(noMoreTsBlockForCurrentGroup.remove(0));
+    if (isNoMoreTsBlockForCurrentGroup) {
+      resetFillState();
+    }
+  }
+
+  @Override
+  public void close() throws Exception {
+    super.close();
+    lastRow = null;
+  }
+
+  @Override
+  void updateCachedData(TsBlock tsBlock) {
+
+    boolean isFirstGroupOfCurrentTsBlock = true;
+    SortKey currentGroupKey = new SortKey(tsBlock, 0);
+    int size = tsBlock.getPositionCount();
+    for (int i = 1; i < size; i++) {
+      SortKey nextGroupKey = new SortKey(tsBlock, i);
+
+      if (groupKeyComparator.compare(currentGroupKey, nextGroupKey) != 0) {
+        int length = i - currentGroupKey.rowIndex;
+        TsBlock currentGroup = tsBlock.getRegion(currentGroupKey.rowIndex, 
length);
+        super.updateCachedData(currentGroup);
+        if (isFirstGroupOfCurrentTsBlock) {
+          isFirstGroupOfCurrentTsBlock = false;
+          boolean isNewGroup = isNewGroup(currentGroupKey);
+          if (isNewGroup && !noMoreTsBlockForCurrentGroup.isEmpty()) {
+            
noMoreTsBlockForCurrentGroup.set(noMoreTsBlockForCurrentGroup.size() - 1, true);
+          } else if (isNewGroup) {
+            resetFillState();
+          }
+        }
+        noMoreTsBlockForCurrentGroup.add(true);
+        currentGroupKey = nextGroupKey;
+      }
+    }
+
+    int length = size - currentGroupKey.rowIndex;
+    TsBlock currentGroup = tsBlock.getRegion(currentGroupKey.rowIndex, length);
+    super.updateCachedData(currentGroup);
+    if (isFirstGroupOfCurrentTsBlock) {
+      boolean isNewGroup = isNewGroup(currentGroupKey);
+      if (isNewGroup && !noMoreTsBlockForCurrentGroup.isEmpty()) {
+        noMoreTsBlockForCurrentGroup.set(noMoreTsBlockForCurrentGroup.size() - 
1, true);
+      } else if (isNewGroup) {
+        resetFillState();
+      }
+    }
+    noMoreTsBlockForCurrentGroup.add(false);
+    lastRow = currentGroupKey;
+  }
+
+  private boolean isNewGroup(SortKey currentGroupKey) {
+    return lastRow == null || groupKeyComparator.compare(lastRow, 
currentGroupKey) != 0;
+  }
+
+  private void resetFillState() {
+    for (ILinearFill fill : fillArray) {
+      fill.reset();
+    }
+  }
+}
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/fill/next/NextFill.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/fill/next/NextFill.java
new file mode 100644
index 00000000000..84ecb7245dc
--- /dev/null
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/fill/next/NextFill.java
@@ -0,0 +1,182 @@
+/*
+ * 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.calc.execution.operator.process.fill.next;
+
+import org.apache.iotdb.calc.execution.operator.process.fill.IFillFilter;
+import org.apache.iotdb.calc.execution.operator.process.fill.ILinearFill;
+
+import org.apache.tsfile.block.column.Column;
+import org.apache.tsfile.read.common.block.column.RunLengthEncodedColumn;
+
+/**
+ * NEXT fill copies the nearest non-null value that appears later in the 
current input order. The
+ * optional filter is only used for TIME_BOUND checks.
+ */
+public abstract class NextFill implements ILinearFill {
+
+  private final IFillFilter filter;
+
+  private long nextRowIndex = -1;
+  private long nextTime = -1;
+  private boolean nextIsNull = true;
+
+  private long nextTimeInCurrentColumn = -1;
+  private boolean nextInCurrentColumnIsNull = true;
+
+  protected NextFill(IFillFilter filter) {
+    this.filter = filter;
+  }
+
+  @Override
+  public Column fill(Column timeColumn, Column valueColumn, long 
startRowIndex) {
+    int size = valueColumn.getPositionCount();
+    if (size == 0 || !valueColumn.mayHaveNull()) {
+      return valueColumn;
+    }
+
+    prepareNextValueInCurrentColumn(startRowIndex, size);
+    if (valueColumn instanceof RunLengthEncodedColumn && filter == null) {
+      return nextInCurrentColumnIsNull
+          ? new RunLengthEncodedColumn(createNullValueColumn(), size)
+          : createRunLengthEncodedFilledValueColumn(size);
+    }
+
+    Object array = createValueArray(size);
+    boolean[] isNull = new boolean[size];
+    boolean hasNullValue = false;
+    for (int i = size - 1; i >= 0; i--) {
+      if (valueColumn.isNull(i)) {
+        if (nextInCurrentColumnIsNull || cannotFillByTimeBound(timeColumn, i)) 
{
+          isNull[i] = true;
+          hasNullValue = true;
+        } else {
+          fillNextValueInCurrentColumn(array, i);
+        }
+      } else {
+        fillValue(valueColumn, i, array);
+        if (canBeNextCandidate(timeColumn, i)) {
+          nextInCurrentColumnIsNull = false;
+          nextTimeInCurrentColumn = filter == null ? -1 : 
timeColumn.getLong(i);
+          updateNextValueInCurrentColumn(valueColumn, i);
+        }
+      }
+    }
+    return createFilledValueColumn(array, isNull, hasNullValue, size);
+  }
+
+  @Override
+  public boolean needPrepareForNext(
+      long rowIndex, Column valueColumn, int 
lastRowIndexForNonNullHelperColumn) {
+    if (valueColumn.getPositionCount() == 0 || !valueColumn.mayHaveNull()) {
+      return false;
+    }
+    if (filter != null && lastRowIndexForNonNullHelperColumn < 0) {
+      return false;
+    }
+    if (!nextIsNull && nextRowIndex > rowIndex) {
+      return false;
+    }
+
+    int lastIndex =
+        filter == null ? valueColumn.getPositionCount() - 1 : 
lastRowIndexForNonNullHelperColumn;
+    if (lastIndex < 0) {
+      return false;
+    }
+
+    boolean hasTrailingNull = false;
+    for (int i = lastIndex; i >= 0; i--) {
+      if (valueColumn.isNull(i)) {
+        hasTrailingNull = true;
+      } else {
+        return hasTrailingNull;
+      }
+    }
+    return hasTrailingNull;
+  }
+
+  @Override
+  public boolean prepareForNext(
+      long startRowIndex, long endRowIndex, Column nextTimeColumn, Column 
nextValueColumn) {
+    if (!nextIsNull && endRowIndex < nextRowIndex) {
+      return true;
+    }
+
+    for (int i = 0, size = nextValueColumn.getPositionCount(); i < size; i++) {
+      if (!nextValueColumn.isNull(i) && canBeNextCandidate(nextTimeColumn, i)) 
{
+        updateNextValue(nextValueColumn, i);
+        nextTime = filter == null ? -1 : nextTimeColumn.getLong(i);
+        nextRowIndex = startRowIndex + i;
+        nextIsNull = false;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void reset() {
+    nextRowIndex = -1;
+    nextTime = -1;
+    nextIsNull = true;
+    nextTimeInCurrentColumn = -1;
+    nextInCurrentColumnIsNull = true;
+  }
+
+  private void prepareNextValueInCurrentColumn(long startRowIndex, int size) {
+    if (!nextIsNull && nextRowIndex >= startRowIndex + size) {
+      nextInCurrentColumnIsNull = false;
+      nextTimeInCurrentColumn = nextTime;
+      updateNextValueInCurrentColumn();
+    } else {
+      nextInCurrentColumnIsNull = true;
+      nextTimeInCurrentColumn = -1;
+    }
+  }
+
+  private boolean canBeNextCandidate(Column timeColumn, int index) {
+    return filter == null || !timeColumn.isNull(index);
+  }
+
+  private boolean cannotFillByTimeBound(Column timeColumn, int index) {
+    return filter != null
+        && (timeColumn.isNull(index)
+            || !filter.needFill(timeColumn.getLong(index), 
nextTimeInCurrentColumn));
+  }
+
+  abstract void fillValue(Column column, int index, Object array);
+
+  abstract void fillNextValueInCurrentColumn(Object array, int index);
+
+  abstract Object createValueArray(int size);
+
+  abstract Column createNullValueColumn();
+
+  abstract Column createRunLengthEncodedFilledValueColumn(int size);
+
+  abstract Column createFilledValueColumn(
+      Object array, boolean[] isNull, boolean hasNullValue, int size);
+
+  abstract void updateNextValue(Column nextValueColumn, int index);
+
+  abstract void updateNextValueInCurrentColumn(Column nextValueColumn, int 
index);
+
+  /** update nextValueInCurrentColumn using value of next Column. */
+  abstract void updateNextValueInCurrentColumn();
+}
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/CommonOperatorUtils.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/CommonOperatorUtils.java
index 8928e2a37da..a332122b257 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/CommonOperatorUtils.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/CommonOperatorUtils.java
@@ -32,6 +32,12 @@ import 
org.apache.iotdb.calc.execution.operator.process.fill.linear.DoubleLinear
 import 
org.apache.iotdb.calc.execution.operator.process.fill.linear.FloatLinearFill;
 import 
org.apache.iotdb.calc.execution.operator.process.fill.linear.IntLinearFill;
 import 
org.apache.iotdb.calc.execution.operator.process.fill.linear.LongLinearFill;
+import 
org.apache.iotdb.calc.execution.operator.process.fill.next.BinaryNextFill;
+import 
org.apache.iotdb.calc.execution.operator.process.fill.next.BooleanNextFill;
+import 
org.apache.iotdb.calc.execution.operator.process.fill.next.DoubleNextFill;
+import 
org.apache.iotdb.calc.execution.operator.process.fill.next.FloatNextFill;
+import org.apache.iotdb.calc.execution.operator.process.fill.next.IntNextFill;
+import org.apache.iotdb.calc.execution.operator.process.fill.next.LongNextFill;
 import 
org.apache.iotdb.calc.execution.operator.process.fill.previous.BinaryPreviousFill;
 import 
org.apache.iotdb.calc.execution.operator.process.fill.previous.BinaryPreviousFillWithTimeDuration;
 import 
org.apache.iotdb.calc.execution.operator.process.fill.previous.BooleanPreviousFill;
@@ -105,40 +111,7 @@ public class CommonOperatorUtils {
       List<TSDataType> inputDataTypes,
       TimeDuration timeDurationThreshold,
       ZoneId zoneId) {
-    IFillFilter filter;
-    if (timeDurationThreshold == null) {
-      filter = null;
-    } else if (!timeDurationThreshold.containsMonth()) {
-      filter = new 
FixedIntervalFillFilter(timeDurationThreshold.nonMonthDuration);
-    } else {
-      switch (TIMESTAMP_PRECISION) {
-        case "ms":
-          filter =
-              new MonthIntervalMSFillFilter(
-                  timeDurationThreshold.monthDuration,
-                  timeDurationThreshold.nonMonthDuration,
-                  zoneId);
-          break;
-        case "us":
-          filter =
-              new MonthIntervalUSFillFilter(
-                  timeDurationThreshold.monthDuration,
-                  timeDurationThreshold.nonMonthDuration,
-                  zoneId);
-          break;
-        case "ns":
-          filter =
-              new MonthIntervalNSFillFilter(
-                  timeDurationThreshold.monthDuration,
-                  timeDurationThreshold.nonMonthDuration,
-                  zoneId);
-          break;
-        default:
-          // this case will never reach
-          throw new UnsupportedOperationException(
-              String.format(QueryMessages.UNSUPPORTED_TIME_PRECISION, 
TIMESTAMP_PRECISION));
-      }
-    }
+    IFillFilter filter = createFillFilter(timeDurationThreshold, zoneId);
 
     IFill[] previousFill = new IFill[inputColumns];
     for (int i = 0; i < inputColumns; i++) {
@@ -188,4 +161,68 @@ public class CommonOperatorUtils {
     }
     return previousFill;
   }
+
+  public static ILinearFill[] getNextFill(
+      int inputColumns,
+      List<TSDataType> inputDataTypes,
+      TimeDuration timeDurationThreshold,
+      ZoneId zoneId) {
+    IFillFilter filter = createFillFilter(timeDurationThreshold, zoneId);
+
+    ILinearFill[] nextFill = new ILinearFill[inputColumns];
+    for (int i = 0; i < inputColumns; i++) {
+      switch (inputDataTypes.get(i)) {
+        case BOOLEAN:
+          nextFill[i] = new BooleanNextFill(filter);
+          break;
+        case TEXT:
+        case STRING:
+        case BLOB:
+        case OBJECT:
+          nextFill[i] = new BinaryNextFill(filter);
+          break;
+        case INT32:
+        case DATE:
+          nextFill[i] = new IntNextFill(filter);
+          break;
+        case INT64:
+        case TIMESTAMP:
+          nextFill[i] = new LongNextFill(filter);
+          break;
+        case FLOAT:
+          nextFill[i] = new FloatNextFill(filter);
+          break;
+        case DOUBLE:
+          nextFill[i] = new DoubleNextFill(filter);
+          break;
+        default:
+          throw new IllegalArgumentException(UNKNOWN_DATATYPE + 
inputDataTypes.get(i));
+      }
+    }
+    return nextFill;
+  }
+
+  private static IFillFilter createFillFilter(TimeDuration 
timeDurationThreshold, ZoneId zoneId) {
+    if (timeDurationThreshold == null) {
+      return null;
+    }
+    if (!timeDurationThreshold.containsMonth()) {
+      return new 
FixedIntervalFillFilter(timeDurationThreshold.nonMonthDuration);
+    }
+    switch (TIMESTAMP_PRECISION) {
+      case "ms":
+        return new MonthIntervalMSFillFilter(
+            timeDurationThreshold.monthDuration, 
timeDurationThreshold.nonMonthDuration, zoneId);
+      case "us":
+        return new MonthIntervalUSFillFilter(
+            timeDurationThreshold.monthDuration, 
timeDurationThreshold.nonMonthDuration, zoneId);
+      case "ns":
+        return new MonthIntervalNSFillFilter(
+            timeDurationThreshold.monthDuration, 
timeDurationThreshold.nonMonthDuration, zoneId);
+      default:
+        // this case will never reach
+        throw new UnsupportedOperationException(
+            String.format(QueryMessages.UNSUPPORTED_TIME_PRECISION, 
TIMESTAMP_PRECISION));
+    }
+  }
 }
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
index f65ba4dbc5d..a59016030e7 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
@@ -34,6 +34,8 @@ import 
org.apache.iotdb.calc.execution.operator.process.TableFillOperator;
 import 
org.apache.iotdb.calc.execution.operator.process.TableLinearFillOperator;
 import 
org.apache.iotdb.calc.execution.operator.process.TableLinearFillWithGroupOperator;
 import org.apache.iotdb.calc.execution.operator.process.TableMergeSortOperator;
+import org.apache.iotdb.calc.execution.operator.process.TableNextFillOperator;
+import 
org.apache.iotdb.calc.execution.operator.process.TableNextFillWithGroupOperator;
 import org.apache.iotdb.calc.execution.operator.process.TableSortOperator;
 import 
org.apache.iotdb.calc.execution.operator.process.TableStreamSortOperator;
 import org.apache.iotdb.calc.execution.operator.process.TableTopKOperator;
@@ -134,6 +136,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearF
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MarkDistinctNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.Measure;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MergeSortNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
@@ -218,6 +221,7 @@ import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.IDENTITY_FI
 import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.TIME_COLUMN_TEMPLATE;
 import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.UNKNOWN_DATATYPE;
 import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.getLinearFill;
+import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.getNextFill;
 import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.getPreviousFill;
 import static 
org.apache.iotdb.calc.utils.constant.SqlConstant.FIRST_AGGREGATION;
 import static 
org.apache.iotdb.calc.utils.constant.SqlConstant.FIRST_BY_AGGREGATION;
@@ -607,6 +611,46 @@ public abstract class TableOperatorGenerator<
     }
   }
 
+  @Override
+  public Operator visitNextFill(NextFillNode node, C context) {
+    Operator child = node.getChild().accept(this, context);
+
+    List<TSDataType> inputDataTypes =
+        getOutputColumnTypes(node.getChild(), context.getTableTypeProvider());
+    int inputColumnCount = inputDataTypes.size();
+    int helperColumnIndex = -1;
+    if (node.getHelperColumn().isPresent()) {
+      helperColumnIndex = getColumnIndex(node.getHelperColumn().get(), 
node.getChild());
+    }
+    ILinearFill[] fillArray =
+        getNextFill(
+            inputColumnCount,
+            inputDataTypes,
+            node.getTimeBound().orElse(null),
+            context.getZoneId());
+
+    if (node.getGroupingKeys().isPresent()) {
+      CommonOperatorContext operatorContext =
+          addOperatorContext(
+              context, node.getPlanNodeId(), 
TableNextFillWithGroupOperator.class.getSimpleName());
+      return new TableNextFillWithGroupOperator(
+          operatorContext,
+          fillArray,
+          child,
+          helperColumnIndex,
+          node.getTimeBound().isPresent(),
+          genFillGroupKeyComparator(
+              node.getGroupingKeys().get(), node, inputDataTypes, new 
HashSet<>()),
+          inputDataTypes);
+    } else {
+      CommonOperatorContext operatorContext =
+          addOperatorContext(
+              context, node.getPlanNodeId(), 
TableNextFillOperator.class.getSimpleName());
+      return new TableNextFillOperator(
+          operatorContext, fillArray, child, helperColumnIndex, 
node.getTimeBound().isPresent());
+    }
+  }
+
   @Override
   public Operator visitValueFill(ValueFillNode node, C context) {
     Operator child = node.getChild().accept(this, context);
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 e38ce40d48b..f747c185035 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
@@ -37,6 +37,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.Interse
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.JoinNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MarkDistinctNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
@@ -914,6 +915,20 @@ public class PlanGraphPrinter implements 
PlanVisitor<List<String>, PlanGraphPrin
     return render(node, boxValue, context);
   }
 
+  @Override
+  public List<String> visitNextFill(NextFillNode node, GraphContext context) {
+    List<String> boxValue = new ArrayList<>();
+    boxValue.add(String.format("NextFill-%s", node.getPlanNodeId().getId()));
+    node.getTimeBound()
+        .ifPresent(timeBound -> boxValue.add(String.format("TIME_BOUND: %s", 
timeBound)));
+    node.getHelperColumn()
+        .ifPresent(timeColumn -> boxValue.add(String.format("TIME_COLUMN: %s", 
timeColumn)));
+    node.getGroupingKeys()
+        .ifPresent(groupingKeys -> boxValue.add(String.format("FILL_GROUP: 
%s", groupingKeys)));
+
+    return render(node, boxValue, context);
+  }
+
   @Override
   public List<String> visitValueFill(ValueFillNode node, GraphContext context) 
{
     List<String> boxValue = new ArrayList<>();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/Analysis.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/Analysis.java
index a10cf6254bb..34ed87bc08f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/Analysis.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/Analysis.java
@@ -1529,6 +1529,32 @@ public class Analysis implements IAnalysis {
     }
   }
 
+  public static class NextFillAnalysis extends FillAnalysis {
+    @Nullable private final TimeDuration timeBound;
+    @Nullable private final FieldReference fieldReference;
+    @Nullable private final List<FieldReference> groupingKeys;
+
+    public NextFillAnalysis(
+        TimeDuration timeBound, FieldReference fieldReference, 
List<FieldReference> groupingKeys) {
+      super(FillPolicy.NEXT);
+      this.timeBound = timeBound;
+      this.fieldReference = fieldReference;
+      this.groupingKeys = groupingKeys;
+    }
+
+    public Optional<TimeDuration> getTimeBound() {
+      return Optional.ofNullable(timeBound);
+    }
+
+    public Optional<FieldReference> getFieldReference() {
+      return Optional.ofNullable(fieldReference);
+    }
+
+    public Optional<List<FieldReference>> getGroupingKeys() {
+      return Optional.ofNullable(groupingKeys);
+    }
+  }
+
   public static class LinearFillAnalysis extends FillAnalysis {
     private final FieldReference fieldReference;
     @Nullable private final List<FieldReference> groupingKeys;
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 d0779ee24a5..8912ac37264 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
@@ -3972,6 +3972,28 @@ public class StatementAnalyzer {
         fillAnalysis =
             new Analysis.PreviousFillAnalysis(
                 node.getTimeBound().orElse(null), timeColumn, groupingKeys);
+      } else if (node.getFillMethod() == FillPolicy.NEXT) {
+        FieldReference timeColumn = null;
+        List<FieldReference> groupingKeys = null;
+        if (node.getTimeBound().isPresent() || 
node.getFillGroupingElements().isPresent()) {
+          timeColumn = getHelperColumn(node, scope, FillPolicy.NEXT);
+          ExpressionAnalyzer.analyzeExpression(
+              metadata,
+              queryContext,
+              sessionContext,
+              statementAnalyzerFactory,
+              accessControl,
+              scope,
+              analysis,
+              timeColumn,
+              WarningCollector.NOOP,
+              correlationSupport);
+
+          groupingKeys = analyzeFillGroup(node, scope, FillPolicy.NEXT);
+        }
+        fillAnalysis =
+            new Analysis.NextFillAnalysis(
+                node.getTimeBound().orElse(null), timeColumn, groupingKeys);
       } else if (node.getFillMethod() == FillPolicy.CONSTANT) {
         Literal literal = node.getFillValue().get();
         ExpressionAnalyzer.analyzeExpression(
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 0232406c4e5..3159ac00300 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
@@ -35,6 +35,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.GapFill
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.GroupNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
@@ -1296,6 +1297,27 @@ public class QueryPlanner {
                 previousFillAnalysis.getTimeBound().orElse(null),
                 previousFillHelperColumn,
                 groupingKeys));
+      case NEXT:
+        Analysis.NextFillAnalysis nextFillAnalysis =
+            (Analysis.NextFillAnalysis) analysis.getFill(fill.get());
+        Symbol nextFillHelperColumn = null;
+        if (nextFillAnalysis.getFieldReference().isPresent()) {
+          nextFillHelperColumn = 
subPlan.translate(nextFillAnalysis.getFieldReference().get());
+        }
+
+        if (nextFillAnalysis.getGroupingKeys().isPresent()) {
+          List<FieldReference> fieldReferenceList = 
nextFillAnalysis.getGroupingKeys().get();
+          groupingKeys = new ArrayList<>(fieldReferenceList.size());
+          subPlan = fillGroup(subPlan, fieldReferenceList, groupingKeys, 
nextFillHelperColumn);
+        }
+
+        return subPlan.withNewRoot(
+            new NextFillNode(
+                queryIdAllocator.genPlanNodeId(),
+                subPlan.getRoot(),
+                nextFillAnalysis.getTimeBound().orElse(null),
+                nextFillHelperColumn,
+                groupingKeys));
       case LINEAR:
         Analysis.LinearFillAnalysis linearFillAnalysis =
             (Analysis.LinearFillAnalysis) analysis.getFill(fill.get());
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneFillColumns.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneFillColumns.java
index d93953c695b..d1a061ea90e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneFillColumns.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneFillColumns.java
@@ -23,6 +23,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
 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.LinearFillNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
 
 import com.google.common.collect.ImmutableSet;
@@ -50,6 +51,10 @@ public class PruneFillColumns extends 
ProjectOffPushDownRule<FillNode> {
       PreviousFillNode previousFillNode = (PreviousFillNode) fillNode;
       previousFillNode.getHelperColumn().ifPresent(referencedInputs::add);
       previousFillNode.getGroupingKeys().ifPresent(keys -> 
referencedInputs.addAll(keys));
+    } else if (fillNode instanceof NextFillNode) {
+      NextFillNode nextFillNode = (NextFillNode) fillNode;
+      nextFillNode.getHelperColumn().ifPresent(referencedInputs::add);
+      nextFillNode.getGroupingKeys().ifPresent(keys -> 
referencedInputs.addAll(keys));
     } else if (fillNode instanceof LinearFillNode) {
       LinearFillNode linearFillNode = (LinearFillNode) fillNode;
       referencedInputs.add(linearFillNode.getHelperColumn());
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushLimitOffsetIntoTableScan.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushLimitOffsetIntoTableScan.java
index d3155343a6a..298e0616f31 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushLimitOffsetIntoTableScan.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushLimitOffsetIntoTableScan.java
@@ -31,6 +31,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.GroupNo
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.JoinNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
@@ -141,6 +142,12 @@ public class PushLimitOffsetIntoTableScan implements 
PlanOptimizer {
       return node;
     }
 
+    @Override
+    public PlanNode visitNextFill(NextFillNode node, Context context) {
+      context.enablePushDown = false;
+      return node;
+    }
+
     @Override
     public PlanNode visitPreviousFill(PreviousFillNode node, Context context) {
       if (node.getGroupingKeys().isPresent()) {
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 b944715fb8f..eeda5d77094 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
@@ -39,6 +39,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.JoinNod
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MarkDistinctNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
@@ -332,6 +333,37 @@ public class UnaliasSymbolReferences implements 
PlanOptimizer {
       }
     }
 
+    @Override
+    public PlanAndMappings visitNextFill(NextFillNode node, UnaliasContext 
context) {
+      PlanAndMappings rewrittenSource = node.getChild().accept(this, context);
+
+      if (node.getHelperColumn().isPresent() || 
node.getGroupingKeys().isPresent()) {
+        Map<Symbol, Symbol> mapping = new 
HashMap<>(rewrittenSource.getMappings());
+        SymbolMapper mapper = symbolMapper(mapping);
+
+        Symbol helperColumn = null;
+        if (node.getHelperColumn().isPresent()) {
+          helperColumn = mapper.map(node.getHelperColumn().get());
+        }
+        List<Symbol> groupingKeys = null;
+        if (node.getGroupingKeys().isPresent()) {
+          groupingKeys = mapper.mapAndDistinct(node.getGroupingKeys().get());
+        }
+        return new PlanAndMappings(
+            new NextFillNode(
+                node.getPlanNodeId(),
+                rewrittenSource.getRoot(),
+                node.getTimeBound().orElse(null),
+                helperColumn,
+                groupingKeys),
+            mapping);
+      } else {
+        return new PlanAndMappings(
+            node.replaceChildren(ImmutableList.of(rewrittenSource.getRoot())),
+            rewrittenSource.getMappings());
+      }
+    }
+
     @Override
     public PlanAndMappings visitLinearFill(LinearFillNode node, UnaliasContext 
context) {
       PlanAndMappings rewrittenSource = node.getChild().accept(this, context);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
index 25d68eba6be..a19168b50f4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
@@ -142,6 +142,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.WithQuery;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ZeroOrMoreQuantifier;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ZeroOrOneQuantifier;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.parser.ParsingException;
+import 
org.apache.iotdb.commons.queryengine.plan.statement.component.FillPolicy;
 import org.apache.iotdb.commons.queryengine.utils.TimestampPrecisionUtils;
 import org.apache.iotdb.commons.schema.cache.CacheClearOptions;
 import org.apache.iotdb.commons.schema.table.InformationSchema;
@@ -2398,6 +2399,43 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
     return new Fill(getLocation(ctx), timeBound, timeColumn, 
fillGroupingElements);
   }
 
+  @Override
+  public Node visitNextFill(RelationalSqlParser.NextFillContext ctx) {
+    TimeDuration timeBound = null;
+    LongLiteral timeColumn = null;
+    List<LongLiteral> fillGroupingElements = null;
+    if (ctx.timeBoundClause() != null) {
+      timeBound =
+          DataNodeDateTimeUtils.constructTimeDuration(
+              ctx.timeBoundClause().timeDuration().getText());
+
+      if (timeBound.monthDuration != 0 && timeBound.nonMonthDuration != 0) {
+        throw new SemanticException(
+            "Simultaneous setting of monthly and non-monthly intervals is not 
supported.");
+      }
+    }
+
+    if (ctx.timeColumnClause() != null) {
+      timeColumn =
+          new LongLiteral(
+              getLocation(ctx.timeColumnClause().INTEGER_VALUE()),
+              ctx.timeColumnClause().INTEGER_VALUE().getText());
+    }
+
+    if (ctx.fillGroupClause() != null) {
+      fillGroupingElements =
+          ctx.fillGroupClause().INTEGER_VALUE().stream()
+              .map(index -> new LongLiteral(getLocation(index), 
index.getText()))
+              .collect(toList());
+    }
+
+    if (timeColumn != null && (timeBound == null && fillGroupingElements == 
null)) {
+      throw new SemanticException(
+          "Don't need to specify TIME_COLUMN while either TIME_BOUND or 
FILL_GROUP parameter is not specified");
+    }
+    return new Fill(getLocation(ctx), FillPolicy.NEXT, timeBound, timeColumn, 
fillGroupingElements);
+  }
+
   @Override
   public Node visitLinearFill(RelationalSqlParser.LinearFillContext ctx) {
     LongLiteral timeColumn = null;
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/fill/NextFillTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/fill/NextFillTest.java
new file mode 100644
index 00000000000..497fe6efe4d
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/fill/NextFillTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.execution.operator.process.fill;
+
+import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
+import org.apache.iotdb.calc.execution.operator.Operator;
+import 
org.apache.iotdb.calc.execution.operator.process.TableNextFillWithGroupOperator;
+import org.apache.iotdb.calc.execution.operator.process.fill.ILinearFill;
+import org.apache.iotdb.calc.plan.planner.CommonOperatorUtils;
+import org.apache.iotdb.calc.plan.planner.memory.MemoryReservationManager;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.block.TsBlock;
+import org.apache.tsfile.read.common.block.TsBlockBuilder;
+import org.apache.tsfile.read.common.block.column.BinaryColumn;
+import org.apache.tsfile.read.common.block.column.IntColumn;
+import org.apache.tsfile.read.common.block.column.LongColumn;
+import org.apache.tsfile.read.common.block.column.RunLengthEncodedColumn;
+import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.TimeDuration;
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class NextFillTest {
+
+  @Test
+  public void testNextFillAcrossTsBlocks() {
+    ILinearFill fill =
+        CommonOperatorUtils.getNextFill(
+            1, ImmutableList.of(TSDataType.INT32), null, 
ZoneId.systemDefault())[0];
+    IntColumn valueColumn =
+        new IntColumn(3, Optional.of(new boolean[] {true, true, true}), new 
int[3]);
+    LongColumn timeColumn = new LongColumn(3, Optional.empty(), new long[] {1, 
2, 3});
+    IntColumn nextValueColumn =
+        new IntColumn(2, Optional.of(new boolean[] {true, false}), new int[] 
{0, 9});
+    LongColumn nextTimeColumn = new LongColumn(2, Optional.empty(), new long[] 
{4, 5});
+
+    assertTrue(fill.needPrepareForNext(2, valueColumn, 2));
+    assertTrue(fill.prepareForNext(3, 2, nextTimeColumn, nextValueColumn));
+
+    IntColumn result = (IntColumn) fill.fill(timeColumn, valueColumn, 0);
+    for (int i = 0; i < result.getPositionCount(); i++) {
+      assertFalse(result.isNull(i));
+      assertEquals(9, result.getInt(i));
+    }
+  }
+
+  @Test
+  public void testNextFillTimeBoundAndHelperNull() {
+    ILinearFill fill =
+        CommonOperatorUtils.getNextFill(
+            1, ImmutableList.of(TSDataType.INT32), new TimeDuration(0, 2), 
ZoneId.systemDefault())[
+            0];
+    IntColumn valueColumn =
+        new IntColumn(3, Optional.of(new boolean[] {true, true, true}), new 
int[3]);
+    LongColumn timeColumn =
+        new LongColumn(3, Optional.of(new boolean[] {false, true, false}), new 
long[] {1, 0, 3});
+    IntColumn nextValueColumn = new IntColumn(1, Optional.empty(), new int[] 
{40});
+    LongColumn nextTimeColumn = new LongColumn(1, Optional.empty(), new long[] 
{4});
+
+    assertTrue(fill.needPrepareForNext(2, valueColumn, 2));
+    assertTrue(fill.prepareForNext(3, 2, nextTimeColumn, nextValueColumn));
+
+    IntColumn result = (IntColumn) fill.fill(timeColumn, valueColumn, 0);
+    assertTrue(result.isNull(0));
+    assertTrue(result.isNull(1));
+    assertFalse(result.isNull(2));
+    assertEquals(40, result.getInt(2));
+  }
+
+  @Test
+  public void testObjectNextFillUsesBinaryFill() {
+    ILinearFill fill =
+        CommonOperatorUtils.getNextFill(
+            1, ImmutableList.of(TSDataType.OBJECT), null, 
ZoneId.systemDefault())[0];
+    Binary objectValue = new Binary("object-value", 
TSFileConfig.STRING_CHARSET);
+    BinaryColumn valueColumn =
+        new BinaryColumn(
+            2,
+            Optional.of(new boolean[] {true, false}),
+            new Binary[] {Binary.EMPTY_VALUE, objectValue});
+    LongColumn timeColumn = new LongColumn(2, Optional.empty(), new long[] {1, 
2});
+
+    BinaryColumn result = (BinaryColumn) fill.fill(timeColumn, valueColumn, 0);
+    assertFalse(result.isNull(0));
+    assertEquals(objectValue, result.getBinary(0));
+    assertEquals(objectValue, result.getBinary(1));
+  }
+
+  @Test
+  public void testNextFillWithGroupDoesNotUseNextGroupAfterContinuedGroup() 
throws Exception {
+    List<TSDataType> dataTypes = ImmutableList.of(TSDataType.TEXT, 
TSDataType.INT32);
+    CommonOperatorContext operatorContext = new TestOperatorContext();
+    TableNextFillWithGroupOperator operator =
+        new TableNextFillWithGroupOperator(
+            operatorContext,
+            CommonOperatorUtils.getNextFill(2, dataTypes, null, 
ZoneId.systemDefault()),
+            new TsBlockSourceOperator(
+                operatorContext,
+                ImmutableList.of(
+                    buildBlock(new String[] {"a"}, new Integer[] {1}),
+                    buildBlock(new String[] {"a", "b"}, new Integer[] {null, 
9}))),
+            -1,
+            false,
+            (left, right) ->
+                left.tsBlock
+                    .getColumn(0)
+                    .getBinary(left.rowIndex)
+                    .toString()
+                    
.compareTo(right.tsBlock.getColumn(0).getBinary(right.rowIndex).toString()),
+            dataTypes);
+
+    assertTrue(operator.hasNext());
+    TsBlock firstBlock = operator.next();
+    assertEquals(1, firstBlock.getPositionCount());
+    assertEquals(1, firstBlock.getColumn(1).getInt(0));
+
+    assertTrue(operator.hasNext());
+    TsBlock secondBlock = operator.next();
+    assertEquals(2, secondBlock.getPositionCount());
+    assertEquals("a", secondBlock.getColumn(0).getBinary(0).toString());
+    assertTrue(secondBlock.getColumn(1).isNull(0));
+    assertEquals("b", secondBlock.getColumn(0).getBinary(1).toString());
+    assertEquals(9, secondBlock.getColumn(1).getInt(1));
+  }
+
+  @Test
+  public void testNextFillWithGroupUsesNextSameGroupAcrossTsBlocks() throws 
Exception {
+    List<TSDataType> dataTypes = ImmutableList.of(TSDataType.TEXT, 
TSDataType.INT32);
+    CommonOperatorContext operatorContext = new TestOperatorContext();
+    TableNextFillWithGroupOperator operator =
+        new TableNextFillWithGroupOperator(
+            operatorContext,
+            CommonOperatorUtils.getNextFill(2, dataTypes, null, 
ZoneId.systemDefault()),
+            new TsBlockSourceOperator(
+                operatorContext,
+                ImmutableList.of(
+                    buildBlock(new String[] {"a"}, new Integer[] {null}),
+                    buildBlock(new String[] {"a"}, new Integer[] {7}))),
+            -1,
+            false,
+            (left, right) ->
+                left.tsBlock
+                    .getColumn(0)
+                    .getBinary(left.rowIndex)
+                    .toString()
+                    
.compareTo(right.tsBlock.getColumn(0).getBinary(right.rowIndex).toString()),
+            dataTypes);
+
+    TsBlock result = nextNonNull(operator);
+    assertNotNull(result);
+    assertEquals(2, result.getPositionCount());
+    assertEquals("a", result.getColumn(0).getBinary(0).toString());
+    assertFalse(result.getColumn(1).isNull(0));
+    assertEquals(7, result.getColumn(1).getInt(0));
+    assertEquals("a", result.getColumn(0).getBinary(1).toString());
+    assertFalse(result.getColumn(1).isNull(1));
+    assertEquals(7, result.getColumn(1).getInt(1));
+    assertFalse(operator.hasNext());
+  }
+
+  private static TsBlock nextNonNull(TableNextFillWithGroupOperator operator) 
throws Exception {
+    while (operator.hasNext()) {
+      TsBlock result = operator.next();
+      if (result != null) {
+        return result;
+      }
+    }
+    return null;
+  }
+
+  private static TsBlock buildBlock(String[] groups, Integer[] values) {
+    TsBlockBuilder builder =
+        new TsBlockBuilder(groups.length, ImmutableList.of(TSDataType.TEXT, 
TSDataType.INT32));
+    for (int i = 0; i < groups.length; i++) {
+      builder.getColumnBuilder(0).writeBinary(new Binary(groups[i], 
TSFileConfig.STRING_CHARSET));
+      if (values[i] == null) {
+        builder.getColumnBuilder(1).appendNull();
+      } else {
+        builder.getColumnBuilder(1).writeInt(values[i]);
+      }
+    }
+    builder.declarePositions(groups.length);
+    return builder.build(
+        new RunLengthEncodedColumn(CommonOperatorUtils.TIME_COLUMN_TEMPLATE, 
groups.length));
+  }
+
+  private static class TsBlockSourceOperator implements Operator {
+
+    private final CommonOperatorContext operatorContext;
+    private final List<TsBlock> tsBlocks;
+    private int index;
+
+    private TsBlockSourceOperator(CommonOperatorContext operatorContext, 
List<TsBlock> tsBlocks) {
+      this.operatorContext = operatorContext;
+      this.tsBlocks = tsBlocks;
+    }
+
+    @Override
+    public CommonOperatorContext getOperatorContext() {
+      return operatorContext;
+    }
+
+    @Override
+    public TsBlock next() {
+      return index < tsBlocks.size() ? tsBlocks.get(index++) : null;
+    }
+
+    @Override
+    public boolean hasNext() {
+      return index < tsBlocks.size();
+    }
+
+    @Override
+    public void close() {
+      // No resources.
+    }
+
+    @Override
+    public boolean isFinished() {
+      return index >= tsBlocks.size();
+    }
+
+    @Override
+    public long calculateMaxPeekMemory() {
+      return 0;
+    }
+
+    @Override
+    public long calculateMaxReturnSize() {
+      return 0;
+    }
+
+    @Override
+    public long calculateRetainedSizeAfterCallingNext() {
+      return 0;
+    }
+
+    @Override
+    public long ramBytesUsed() {
+      return 0;
+    }
+  }
+
+  private static class TestOperatorContext extends CommonOperatorContext {
+
+    private TestOperatorContext() {
+      super(0, new PlanNodeId("test"), "test");
+    }
+
+    @Override
+    public MemoryReservationManager getMemoryReservationContext() {
+      return null;
+    }
+
+    @Override
+    public int getFragmentId() {
+      return 0;
+    }
+
+    @Override
+    public int getPipelineId() {
+      return 0;
+    }
+
+    @Override
+    public long ramBytesUsed() {
+      return 0;
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/NextFillNodeSerdeTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/NextFillNodeSerdeTest.java
new file mode 100644
index 00000000000..96d57ca6b9b
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/NextFillNodeSerdeTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
+import 
org.apache.iotdb.db.queryengine.plan.planner.node.PlanNodeDeserializeHelper;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.tsfile.utils.TimeDuration;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+public class NextFillNodeSerdeTest {
+
+  @Test
+  public void testNextFillNodeSerde() throws Exception {
+    NextFillNode node =
+        new NextFillNode(
+            new PlanNodeId("nextFill"),
+            null,
+            new TimeDuration(0, 2),
+            new Symbol("time"),
+            ImmutableList.of(new Symbol("city")));
+
+    NextFillNode deserialized = assertNextFillNodeSerde(node);
+    assertTrue(deserialized.getTimeBound().isPresent());
+    assertEquals(new TimeDuration(0, 2), deserialized.getTimeBound().get());
+    assertTrue(deserialized.getHelperColumn().isPresent());
+    assertEquals(new Symbol("time"), deserialized.getHelperColumn().get());
+    assertTrue(deserialized.getGroupingKeys().isPresent());
+    assertEquals(ImmutableList.of(new Symbol("city")), 
deserialized.getGroupingKeys().get());
+  }
+
+  @Test
+  public void testPlainNextFillNodeSerde() throws Exception {
+    NextFillNode node = new NextFillNode(new PlanNodeId("plainNextFill"), 
null, null, null, null);
+
+    NextFillNode deserialized = assertNextFillNodeSerde(node);
+    assertFalse(deserialized.getTimeBound().isPresent());
+    assertFalse(deserialized.getHelperColumn().isPresent());
+    assertFalse(deserialized.getGroupingKeys().isPresent());
+  }
+
+  @Test
+  public void testPartialNextFillNodeSerde() throws Exception {
+    NextFillNode node =
+        new NextFillNode(
+            new PlanNodeId("partialNextFill"),
+            null,
+            new TimeDuration(0, 5),
+            new Symbol("time"),
+            null);
+
+    NextFillNode deserialized = assertNextFillNodeSerde(node);
+    assertTrue(deserialized.getTimeBound().isPresent());
+    assertEquals(new TimeDuration(0, 5), deserialized.getTimeBound().get());
+    assertTrue(deserialized.getHelperColumn().isPresent());
+    assertEquals(new Symbol("time"), deserialized.getHelperColumn().get());
+    assertFalse(deserialized.getGroupingKeys().isPresent());
+  }
+
+  @Test
+  public void testPreviousFillNodeEqualsIncludesGroupingKeys() {
+    PreviousFillNode cityGroupNode =
+        new PreviousFillNode(
+            new PlanNodeId("previousFill"), null, null, null, 
ImmutableList.of(new Symbol("city")));
+    PreviousFillNode deviceGroupNode =
+        new PreviousFillNode(
+            new PlanNodeId("previousFill"),
+            null,
+            null,
+            null,
+            ImmutableList.of(new Symbol("device")));
+
+    assertNotEquals(cityGroupNode, deviceGroupNode);
+    assertNotEquals(cityGroupNode.hashCode(), deviceGroupNode.hashCode());
+  }
+
+  private static NextFillNode assertNextFillNodeSerde(NextFillNode node) 
throws Exception {
+    ByteBuffer byteBuffer = node.serializeToByteBuffer();
+    PlanNode deserialized = PlanNodeDeserializeHelper.deserialize(byteBuffer);
+    assertEquals(node, deserialized);
+    return (NextFillNode) deserialized;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/FillStatementTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/FillStatementTest.java
new file mode 100644
index 00000000000..59d26aedb12
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/FillStatementTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.sql.parser;
+
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Fill;
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Query;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.QuerySpecification;
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement;
+import 
org.apache.iotdb.commons.queryengine.plan.statement.component.FillPolicy;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.InternalClientSession;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.ZoneId;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class FillStatementTest {
+
+  private SqlParser sqlParser;
+  private IClientSession clientSession;
+
+  @Before
+  public void setUp() {
+    sqlParser = new SqlParser();
+    clientSession = new InternalClientSession("testClient");
+  }
+
+  @Test
+  public void testNextFillStatement() {
+    Statement statement =
+        sqlParser.createStatement(
+            "select time, device_id, s1 from table1 FILL METHOD NEXT 
TIME_BOUND 2ms TIME_COLUMN 1 FILL_GROUP 2,3",
+            ZoneId.systemDefault(),
+            clientSession);
+
+    assertTrue(statement instanceof Query);
+    QuerySpecification querySpecification = (QuerySpecification) ((Query) 
statement).getQueryBody();
+    assertTrue(querySpecification.getFill().isPresent());
+    Fill fill = querySpecification.getFill().get();
+    assertEquals(FillPolicy.NEXT, fill.getFillMethod());
+    assertTrue(fill.getTimeBound().isPresent());
+    assertEquals(1, fill.getTimeColumnIndex().get().getParsedValue());
+    assertEquals(2, fill.getFillGroupingElements().get().size());
+    assertEquals(2, 
fill.getFillGroupingElements().get().get(0).getParsedValue());
+    assertEquals(3, 
fill.getFillGroupingElements().get().get(1).getParsedValue());
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/CommonPlanNodeDeserializer.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/CommonPlanNodeDeserializer.java
index 8f14053cd4c..6219a053417 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/CommonPlanNodeDeserializer.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/CommonPlanNodeDeserializer.java
@@ -30,6 +30,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.JoinNod
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MarkDistinctNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MergeSortNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PreviousFillNode;
@@ -140,6 +141,8 @@ public class CommonPlanNodeDeserializer implements 
IPlanNodeDeserializer {
         return RowNumberNode.deserialize(buffer);
       case 1039:
         return ValuesNode.deserialize(buffer);
+      case 1043:
+        return NextFillNode.deserialize(buffer);
       default:
         throw new IllegalArgumentException(QueryMessages.INVALID_NODE_TYPE + 
nodeType);
     }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/ICoreQueryPlanVisitor.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/ICoreQueryPlanVisitor.java
index c1b884088de..ed429b227a9 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/ICoreQueryPlanVisitor.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/ICoreQueryPlanVisitor.java
@@ -41,6 +41,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNo
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LinearFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MarkDistinctNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MergeSortNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.NextFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
@@ -154,6 +155,10 @@ public interface ICoreQueryPlanVisitor<R, C> extends 
IPlanVisitor<R, C> {
     return visitFill(node, context);
   }
 
+  default R visitNextFill(NextFillNode node, C context) {
+    return visitFill(node, context);
+  }
+
   default R visitValueFill(ValueFillNode node, C context) {
     return visitFill(node, context);
   }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java
index 71eb2238f15..76a3b2f5e28 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java
@@ -200,6 +200,7 @@ public enum PlanNodeType {
   TABLE_DISK_USAGE_INFORMATION_SCHEMA_TABLE_SCAN_NODE((short) 1040),
   ALIGNED_AGGREGATION_TREE_DEVICE_VIEW_SCAN_NODE((short) 1041),
   NON_ALIGNED_AGGREGATION_TREE_DEVICE_VIEW_SCAN_NODE((short) 1042),
+  TABLE_NEXT_FILL_NODE((short) 1043),
 
   RELATIONAL_INSERT_TABLET((short) 2000),
   RELATIONAL_INSERT_ROW((short) 2001),
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/NextFillNode.java
similarity index 88%
copy from 
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
copy to 
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/NextFillNode.java
index 36b66835d2d..11676253b33 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/NextFillNode.java
@@ -40,7 +40,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 
-public class PreviousFillNode extends FillNode {
+public class NextFillNode extends FillNode {
 
   @Nullable private final TimeDuration timeBound;
 
@@ -48,7 +48,7 @@ public class PreviousFillNode extends FillNode {
 
   @Nullable private final List<Symbol> groupingKeys;
 
-  public PreviousFillNode(
+  public NextFillNode(
       PlanNodeId id,
       PlanNode child,
       TimeDuration timeBound,
@@ -74,17 +74,17 @@ public class PreviousFillNode extends FillNode {
 
   @Override
   public PlanNode clone() {
-    return new PreviousFillNode(id, null, timeBound, helperColumn, 
groupingKeys);
+    return new NextFillNode(id, null, timeBound, helperColumn, groupingKeys);
   }
 
   @Override
   public <R, C> R accept(IPlanVisitor<R, C> visitor, C context) {
-    return ((ICoreQueryPlanVisitor<R, C>) visitor).visitPreviousFill(this, 
context);
+    return ((ICoreQueryPlanVisitor<R, C>) visitor).visitNextFill(this, 
context);
   }
 
   @Override
   protected void serializeAttributes(ByteBuffer byteBuffer) {
-    PlanNodeType.TABLE_PREVIOUS_FILL_NODE.serialize(byteBuffer);
+    PlanNodeType.TABLE_NEXT_FILL_NODE.serialize(byteBuffer);
     if (timeBound == null) {
       ReadWriteIOUtils.write(false, byteBuffer);
     } else {
@@ -112,7 +112,7 @@ public class PreviousFillNode extends FillNode {
 
   @Override
   protected void serializeAttributes(DataOutputStream stream) throws 
IOException {
-    PlanNodeType.TABLE_PREVIOUS_FILL_NODE.serialize(stream);
+    PlanNodeType.TABLE_NEXT_FILL_NODE.serialize(stream);
     if (timeBound == null) {
       ReadWriteIOUtils.write(false, stream);
     } else {
@@ -136,7 +136,7 @@ public class PreviousFillNode extends FillNode {
     }
   }
 
-  public static PreviousFillNode deserialize(ByteBuffer byteBuffer) {
+  public static NextFillNode deserialize(ByteBuffer byteBuffer) {
     boolean hasValue = ReadWriteIOUtils.readBool(byteBuffer);
     TimeDuration timeDuration = null;
     if (hasValue) {
@@ -157,12 +157,12 @@ public class PreviousFillNode extends FillNode {
       }
     }
     PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer);
-    return new PreviousFillNode(planNodeId, null, timeDuration, helperColumn, 
groupingKeys);
+    return new NextFillNode(planNodeId, null, timeDuration, helperColumn, 
groupingKeys);
   }
 
   @Override
   public PlanNode replaceChildren(List<PlanNode> newChildren) {
-    return new PreviousFillNode(
+    return new NextFillNode(
         id, Iterables.getOnlyElement(newChildren), timeBound, helperColumn, 
groupingKeys);
   }
 
@@ -177,18 +177,19 @@ public class PreviousFillNode extends FillNode {
     if (!super.equals(o)) {
       return false;
     }
-    PreviousFillNode that = (PreviousFillNode) o;
+    NextFillNode that = (NextFillNode) o;
     return Objects.equals(timeBound, that.timeBound)
-        && Objects.equals(helperColumn, that.helperColumn);
+        && Objects.equals(helperColumn, that.helperColumn)
+        && Objects.equals(groupingKeys, that.groupingKeys);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(super.hashCode(), timeBound, helperColumn);
+    return Objects.hash(super.hashCode(), timeBound, helperColumn, 
groupingKeys);
   }
 
   @Override
   public String toString() {
-    return "PreviousFillNode-" + this.getPlanNodeId();
+    return "NextFillNode-" + this.getPlanNodeId();
   }
 }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
index 36b66835d2d..950425acfeb 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/PreviousFillNode.java
@@ -179,12 +179,13 @@ public class PreviousFillNode extends FillNode {
     }
     PreviousFillNode that = (PreviousFillNode) o;
     return Objects.equals(timeBound, that.timeBound)
-        && Objects.equals(helperColumn, that.helperColumn);
+        && Objects.equals(helperColumn, that.helperColumn)
+        && Objects.equals(groupingKeys, that.groupingKeys);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(super.hashCode(), timeBound, helperColumn);
+    return Objects.hash(super.hashCode(), timeBound, helperColumn, 
groupingKeys);
   }
 
   @Override
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/ast/Fill.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/ast/Fill.java
index c544b2eac7e..303100aa740 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/ast/Fill.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/ast/Fill.java
@@ -41,13 +41,13 @@ public class Fill extends Node {
   // used for constant fill
   private final Literal fillValue;
 
-  // used for previous fill
+  // used for previous fill or next fill
   private final TimeDuration timeBound;
 
-  // used for linear fill or previous fill
+  // used for linear fill, previous fill, or next fill
   private final LongLiteral timeColumnIndex;
 
-  // used for linear fill or previous fill
+  // used for linear fill, previous fill, or next fill
   private final List<LongLiteral> fillGroupingElements;
 
   // used for constant fill
@@ -66,11 +66,25 @@ public class Fill extends Node {
       TimeDuration timeBound,
       LongLiteral timeColumnIndex,
       List<LongLiteral> fillGroupingElements) {
+    this(location, FillPolicy.PREVIOUS, timeBound, timeColumnIndex, 
fillGroupingElements);
+  }
+
+  // used for previous fill or next fill
+  public Fill(
+      NodeLocation location,
+      FillPolicy fillMethod,
+      TimeDuration timeBound,
+      LongLiteral timeColumnIndex,
+      List<LongLiteral> fillGroupingElements) {
     super(requireNonNull(location, "location is null"));
+    fillMethod = requireNonNull(fillMethod, "fillMethod is null");
+    if (fillMethod != FillPolicy.PREVIOUS && fillMethod != FillPolicy.NEXT) {
+      throw new IllegalArgumentException("Unsupported fill method: " + 
fillMethod);
+    }
     this.fillValue = null;
     this.timeBound = timeBound;
     this.timeColumnIndex = timeColumnIndex;
-    this.fillMethod = FillPolicy.PREVIOUS;
+    this.fillMethod = fillMethod;
     this.fillGroupingElements = fillGroupingElements;
   }
 
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/util/CommonQuerySqlFormatter.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/util/CommonQuerySqlFormatter.java
index c9ee3a682d8..7bb325fbc63 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/util/CommonQuerySqlFormatter.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/sql/util/CommonQuerySqlFormatter.java
@@ -158,7 +158,8 @@ public class CommonQuerySqlFormatter implements 
CommonQueryAstVisitor<Void, Inte
                           elements.stream()
                               .map(CommonQuerySqlFormatter::formatExpression)
                               .collect(joining(", "))));
-    } else if (node.getFillMethod() == FillPolicy.PREVIOUS) {
+    } else if (node.getFillMethod() == FillPolicy.PREVIOUS
+        || node.getFillMethod() == FillPolicy.NEXT) {
       node.getTimeBound()
           .ifPresent(timeBound -> builder.append(" TIME_BOUND 
").append(timeBound.toString()));
       node.getTimeColumnIndex()
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/statement/component/FillPolicy.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/statement/component/FillPolicy.java
index 507eda6d1b8..46ba4f308d0 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/statement/component/FillPolicy.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/statement/component/FillPolicy.java
@@ -23,6 +23,7 @@ public enum FillPolicy {
   PREVIOUS((byte) 0),
   LINEAR((byte) 1),
   CONSTANT((byte) 2),
+  NEXT((byte) 3),
   ;
 
   FillPolicy(byte fillMethod) {
diff --git 
a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
 
b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
index f984e825fc7..93dfa8bb1f4 100644
--- 
a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
+++ 
b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
@@ -981,6 +981,7 @@ fillClause
 fillMethod
     : LINEAR timeColumnClause? fillGroupClause?                                
    #linearFill
     | PREVIOUS timeBoundClause? timeColumnClause? fillGroupClause?             
    #previousFill
+    | NEXT timeBoundClause? timeColumnClause? fillGroupClause?                 
    #nextFill
     | CONSTANT literalExpression                                               
    #valueFill
     ;
 


Reply via email to