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 66e4b98ebd8 Optimize TopK query of TableModel by RuntimeFilter (#18204)
66e4b98ebd8 is described below

commit 66e4b98ebd8e9300c9e81c656e7e9eec4108b7f9
Author: Weihao Li <[email protected]>
AuthorDate: Thu Jul 16 09:06:34 2026 +0800

    Optimize TopK query of TableModel by RuntimeFilter (#18204)
---
 .../recent/IoTDBTopKRuntimeFilterScanPathIT.java   | 353 +++++++++++++++++++++
 .../org/apache/iotdb/calc/i18n/CalcMessages.java   |   2 +
 .../org/apache/iotdb/calc/i18n/CalcMessages.java   |   2 +
 .../calc/execution/filter/TopKRuntimeFilter.java   |  67 ++++
 .../operator/process/TableTopKOperator.java        |  40 ++-
 .../execution/operator/process/TopKOperator.java   |  36 +++
 .../calc/plan/planner/TableOperatorGenerator.java  |  25 ++
 .../execution/filter/TopKRuntimeFilterTest.java    |  73 +++++
 .../java/org/apache/iotdb/db/conf/IoTDBConfig.java |  14 +
 .../org/apache/iotdb/db/conf/IoTDBDescriptor.java  |  12 +
 .../execution/fragment/DataNodeQueryContext.java   |  21 ++
 .../execution/operator/source/SeriesScanUtil.java  | 133 +++++++-
 .../relational/AbstractTableScanOperator.java      |  23 +-
 .../ExternalTsFileTableScanOperator.java           |  13 +-
 .../planner/DataNodeTableOperatorGenerator.java    | 158 ++++++++-
 .../plan/planner/plan/node/PlanGraphPrinter.java   |   7 +
 .../planner/plan/parameter/SeriesScanOptions.java  |  19 +-
 .../distribute/TableDistributedPlanGenerator.java  |  11 +-
 .../distribute/TableDistributedPlanner.java        |   5 +-
 .../planner/node/DeviceTableScanNode.java          |  49 ++-
 .../optimizations/LogicalOptimizeFactory.java      |   1 +
 .../optimizations/TopKRuntimeFilterOptimizer.java  |  83 +++++
 .../dataregion/read/QueryDataSource.java           | 114 +++++++
 ...alTsFileTableScanOperatorRuntimeFilterTest.java | 127 ++++++++
 .../planner/assertions/PlanMatchPattern.java       |  25 ++
 .../TopKRuntimeFilterDistributedPlanTest.java      | 200 ++++++++++++
 .../TopKRuntimeFilterTreeViewPlanTest.java         | 119 +++++++
 .../read/QueryDataSourceRuntimeFilterTest.java     |  76 +++++
 .../conf/iotdb-system.properties.template          |   8 +
 .../plan/relational/planner/node/TopKNode.java     |  55 +++-
 30 files changed, 1831 insertions(+), 40 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTopKRuntimeFilterScanPathIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTopKRuntimeFilterScanPathIT.java
new file mode 100644
index 00000000000..7697bc81649
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTopKRuntimeFilterScanPathIT.java
@@ -0,0 +1,353 @@
+/*
+ * 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.relational.it.query.recent;
+
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+import org.apache.iotdb.itbase.env.BaseEnv;
+
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.exception.write.WriteProcessException;
+import org.apache.tsfile.file.metadata.TableSchema;
+import org.apache.tsfile.write.TsFileWriter;
+import org.apache.tsfile.write.record.Tablet;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
+import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
+public class IoTDBTopKRuntimeFilterScanPathIT {
+
+  private static final String TREE_VIEW_DATABASE = "db_topk_rf_view";
+  private static final String TABLE_DATABASE = "db_topk_rf_table";
+  private static final String READ_TSFILE_DATABASE = "db_topk_rf_read_tsfile";
+
+  private static File tmpDir;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    EnvFactory.getEnv().initClusterEnvironment();
+    tmpDir = new File(Files.createTempDirectory("topk-rf-scan-path").toUri());
+    insertTreeModelData();
+    createTreeView();
+    createTableModelDatabase();
+    createReadTsFileDatabase();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    if (tmpDir != null) {
+      deleteDirectory(tmpDir);
+    }
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void orderByTimeLimitOnTreeView() {
+    String[] expectedHeader = new String[] {"time", "device_id", "s1"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.032Z,d3,333,", 
"1970-01-01T00:00:00.031Z,d2,222,",
+        };
+    tableResultSetEqualTest(
+        "SELECT * FROM table1 ORDER BY time DESC LIMIT 2",
+        expectedHeader,
+        retArray,
+        TREE_VIEW_DATABASE);
+  }
+
+  @Test
+  public void orderByTimeAscLimitOnTreeView() {
+    String[] expectedHeader = new String[] {"time", "device_id", "s1"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.010Z,d1,1,", "1970-01-01T00:00:00.011Z,d2,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT * FROM table1 ORDER BY time ASC LIMIT 2",
+        expectedHeader,
+        retArray,
+        TREE_VIEW_DATABASE);
+  }
+
+  @Test
+  public void orderByTimeDescLimitOnTable() {
+    String[] expectedHeader = new String[] {"time", "device_id", "s1"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.032Z,d3,333,", 
"1970-01-01T00:00:00.031Z,d2,222,",
+        };
+    tableResultSetEqualTest(
+        "SELECT * FROM table1 ORDER BY time DESC LIMIT 2",
+        expectedHeader,
+        retArray,
+        TABLE_DATABASE);
+  }
+
+  @Test
+  public void orderByTimeAscLimitOnTable() {
+    String[] expectedHeader = new String[] {"time", "device_id", "s1"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.010Z,d1,1,", "1970-01-01T00:00:00.011Z,d2,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT * FROM table1 ORDER BY time ASC LIMIT 2", expectedHeader, 
retArray, TABLE_DATABASE);
+  }
+
+  @Test
+  public void unionAllSiblingTopKOrderByTimeLimit() {
+    String[] expectedHeader = new String[] {"time", "device_id", "s1"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.030Z,d1,111,", 
"1970-01-01T00:00:00.032Z,d3,333,",
+        };
+    tableResultSetEqualTest(
+        "(SELECT time, device_id, s1 FROM table1 WHERE device_id = 'd1' ORDER 
BY time DESC LIMIT 1)"
+            + " UNION ALL"
+            + " (SELECT time, device_id, s1 FROM table1 WHERE device_id = 'd3' 
ORDER BY time DESC LIMIT 1)",
+        expectedHeader,
+        retArray,
+        TABLE_DATABASE);
+  }
+
+  @Test
+  public void orderByTimeAscLimitOnReadTsFile() throws Exception {
+    File tsFile = new File(tmpDir, "topk-rf-asc.tsfile");
+    try (TsFileWriter writer = new TsFileWriter(tsFile)) {
+      generateTableWithDistinctDeviceTimes(
+          writer, "table1", Arrays.asList("tag1", "tag2"), Arrays.asList("s1", 
"s2"), 1, 3);
+    }
+
+    String[] expectedHeader = new String[] {"time", "tag1", "tag2", "s1", 
"s2"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.001Z,tag1_1,tag2_1,1,1,",
+          "1970-01-01T00:00:00.002Z,tag1_2,tag2_2,2,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT time, tag1, tag2, s1, s2 FROM read_tsfile(PATHS => '"
+            + toSqlPath(tsFile)
+            + "', TABLE_NAME => 'table1') ORDER BY time ASC LIMIT 2",
+        expectedHeader,
+        retArray,
+        READ_TSFILE_DATABASE);
+  }
+
+  @Test
+  public void orderByTimeLimitOnReadTsFile() throws Exception {
+    File tsFile = new File(tmpDir, "topk-rf.tsfile");
+    try (TsFileWriter writer = new TsFileWriter(tsFile)) {
+      generateTableWithDistinctDeviceTimes(
+          writer, "table1", Arrays.asList("tag1", "tag2"), Arrays.asList("s1", 
"s2"), 1, 3);
+    }
+
+    String[] expectedHeader = new String[] {"time", "tag1", "tag2", "s1", 
"s2"};
+    String[] retArray =
+        new String[] {
+          "1970-01-01T00:00:00.003Z,tag1_3,tag2_3,3,3,",
+          "1970-01-01T00:00:00.002Z,tag1_2,tag2_2,2,2,",
+        };
+    tableResultSetEqualTest(
+        "SELECT time, tag1, tag2, s1, s2 FROM read_tsfile(PATHS => '"
+            + toSqlPath(tsFile)
+            + "', TABLE_NAME => 'table1') ORDER BY time DESC LIMIT 2",
+        expectedHeader,
+        retArray,
+        READ_TSFILE_DATABASE);
+  }
+
+  private static void insertTreeModelData() throws Exception {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE root.db_topk_rf");
+      statement.execute(
+          "CREATE TIMESERIES root.db_topk_rf.d1.s1 WITH DATATYPE=INT32, 
ENCODING=RLE");
+      statement.execute(
+          "CREATE TIMESERIES root.db_topk_rf.d2.s1 WITH DATATYPE=INT32, 
ENCODING=RLE");
+      statement.execute(
+          "CREATE TIMESERIES root.db_topk_rf.d3.s1 WITH DATATYPE=INT32, 
ENCODING=RLE");
+      statement.execute(
+          "INSERT INTO root.db_topk_rf.d1(timestamp,s1) VALUES(10, 1), (20, 
11), (30, 111)");
+      statement.execute(
+          "INSERT INTO root.db_topk_rf.d2(timestamp,s1) VALUES(11, 2), (21, 
22), (31, 222)");
+      statement.execute(
+          "INSERT INTO root.db_topk_rf.d3(timestamp,s1) VALUES(12, 3), (22, 
33), (32, 333)");
+    }
+  }
+
+  private static void createTreeView() throws Exception {
+    try (Connection connection = EnvFactory.getEnv().getTableConnection();
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE " + TREE_VIEW_DATABASE);
+      statement.execute("USE " + TREE_VIEW_DATABASE);
+      statement.execute(
+          "CREATE VIEW table1(device_id STRING TAG, s1 INT32 FIELD) AS 
root.db_topk_rf.**");
+    }
+  }
+
+  private static void createTableModelDatabase() throws Exception {
+    prepareTableData(
+        new String[] {
+          "CREATE DATABASE " + TABLE_DATABASE,
+          "USE " + TABLE_DATABASE,
+          "CREATE TABLE table1(device_id STRING TAG, s1 INT32 FIELD)",
+          "INSERT INTO table1(time, device_id, s1) VALUES (10, 'd1', 1), (20, 
'd1', 11), (30, 'd1', 111)",
+          "INSERT INTO table1(time, device_id, s1) VALUES (11, 'd2', 2), (21, 
'd2', 22), (31, 'd2', 222)",
+          "INSERT INTO table1(time, device_id, s1) VALUES (12, 'd3', 3), (22, 
'd3', 33), (32, 'd3', 333)",
+        });
+  }
+
+  private static void createReadTsFileDatabase() throws Exception {
+    try (Connection connection = EnvFactory.getEnv().getTableConnection();
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE " + READ_TSFILE_DATABASE);
+    }
+  }
+
+  private static void generateTableWithDistinctDeviceTimes(
+      TsFileWriter writer,
+      String tableName,
+      List<String> tagColumns,
+      List<String> fieldColumns,
+      int deviceStart,
+      int deviceEnd)
+      throws IOException, WriteProcessException {
+    List<String> columnNames = new ArrayList<>(tagColumns.size() + 
fieldColumns.size());
+    List<TSDataType> columnTypes = new ArrayList<>(tagColumns.size() + 
fieldColumns.size());
+    List<ColumnCategory> columnCategories =
+        new ArrayList<>(tagColumns.size() + fieldColumns.size());
+    for (String tagColumn : tagColumns) {
+      columnNames.add(tagColumn);
+      columnTypes.add(TSDataType.STRING);
+      columnCategories.add(ColumnCategory.TAG);
+    }
+    for (String fieldColumn : fieldColumns) {
+      columnNames.add(fieldColumn);
+      columnTypes.add(TSDataType.INT32);
+      columnCategories.add(ColumnCategory.FIELD);
+    }
+
+    writer.registerTableSchema(
+        new TableSchema(tableName, columnNames, columnTypes, 
columnCategories));
+    Tablet tablet = new Tablet(tableName, columnNames, columnTypes, 
columnCategories);
+    for (int deviceIndex = deviceStart; deviceIndex <= deviceEnd; 
deviceIndex++) {
+      int time = deviceIndex;
+      int row = tablet.getRowSize();
+      tablet.addTimestamp(row, time);
+      for (int i = 0; i < tagColumns.size(); i++) {
+        tablet.addValue(row, i, tagColumns.get(i) + "_" + deviceIndex);
+      }
+      for (int i = 0; i < fieldColumns.size(); i++) {
+        tablet.addValue(row, tagColumns.size() + i, time);
+      }
+      if (tablet.getRowSize() == tablet.getMaxRowNumber()) {
+        writer.writeTable(tablet);
+        tablet.reset();
+      }
+    }
+    if (tablet.getRowSize() != 0) {
+      writer.writeTable(tablet);
+    }
+  }
+
+  private static void generateTable(
+      TsFileWriter writer,
+      String tableName,
+      List<String> tagColumns,
+      List<String> fieldColumns,
+      int deviceStart,
+      int deviceEnd)
+      throws IOException, WriteProcessException {
+    List<String> columnNames = new ArrayList<>(tagColumns.size() + 
fieldColumns.size());
+    List<TSDataType> columnTypes = new ArrayList<>(tagColumns.size() + 
fieldColumns.size());
+    List<ColumnCategory> columnCategories =
+        new ArrayList<>(tagColumns.size() + fieldColumns.size());
+    for (String tagColumn : tagColumns) {
+      columnNames.add(tagColumn);
+      columnTypes.add(TSDataType.STRING);
+      columnCategories.add(ColumnCategory.TAG);
+    }
+    for (String fieldColumn : fieldColumns) {
+      columnNames.add(fieldColumn);
+      columnTypes.add(TSDataType.INT32);
+      columnCategories.add(ColumnCategory.FIELD);
+    }
+
+    writer.registerTableSchema(
+        new TableSchema(tableName, columnNames, columnTypes, 
columnCategories));
+    Tablet tablet = new Tablet(tableName, columnNames, columnTypes, 
columnCategories);
+    for (int deviceIndex = deviceStart; deviceIndex <= deviceEnd; 
deviceIndex++) {
+      for (int time = 1; time <= 3; time++) {
+        int row = tablet.getRowSize();
+        tablet.addTimestamp(row, time);
+        for (int i = 0; i < tagColumns.size(); i++) {
+          tablet.addValue(row, i, tagColumns.get(i) + "_" + deviceIndex);
+        }
+        for (int i = 0; i < fieldColumns.size(); i++) {
+          tablet.addValue(row, tagColumns.size() + i, time);
+        }
+        if (tablet.getRowSize() == tablet.getMaxRowNumber()) {
+          writer.writeTable(tablet);
+          tablet.reset();
+        }
+      }
+    }
+    if (tablet.getRowSize() != 0) {
+      writer.writeTable(tablet);
+    }
+  }
+
+  private static String toSqlPath(File file) {
+    return file.getAbsolutePath().replace("\\", "\\\\").replace("'", "''");
+  }
+
+  private static void deleteDirectory(File directory) {
+    File[] files = directory.listFiles();
+    if (files != null) {
+      for (File file : files) {
+        if (file.isDirectory()) {
+          deleteDirectory(file);
+        } else {
+          file.delete();
+        }
+      }
+    }
+    directory.delete();
+  }
+}
diff --git 
a/iotdb-core/calc-commons/src/main/i18n/en/org/apache/iotdb/calc/i18n/CalcMessages.java
 
b/iotdb-core/calc-commons/src/main/i18n/en/org/apache/iotdb/calc/i18n/CalcMessages.java
index 05b1686e548..9e22e34ceb9 100644
--- 
a/iotdb-core/calc-commons/src/main/i18n/en/org/apache/iotdb/calc/i18n/CalcMessages.java
+++ 
b/iotdb-core/calc-commons/src/main/i18n/en/org/apache/iotdb/calc/i18n/CalcMessages.java
@@ -405,6 +405,8 @@ public final class CalcMessages {
   public static final String 
EXCEPTION_THE_SIZE_OF_READYQUEUE_CANNOT_BE_NEGATIVE_DOT_01D8D0CB = "The size of 
readyQueue cannot be negative.";
   public static final String 
EXCEPTION_THE_SIZE_BETWEEN_WHENTRANSFORMERS_AND_THENTRANSFORMERS_NEEDS_TO_BE_SAME_AC796883
 = "the size between whenTransformers and thenTransformers needs to be same";
   public static final String 
EXCEPTION_EXCEED_MAX_CALL_TIMES_OF_GETCOLUMN_69C77C7E = "Exceed max call times 
of getColumn";
+  public static final String 
EXCEPTION_TOPK_RUNTIME_FILTER_REQUIRES_SINGLE_TIME_ORDER_BY_7EED6208 =
+      "TopK runtime filter requires ORDER BY time on a single INT64 or 
TIMESTAMP column";
   public static final String 
EXCEPTION_FILTER_IS_NOT_SUPPORTED_IN_ARG_DOT_FILTER_IS_ARG_DOT_417C4F3C = 
"Filter is not supported in %s. Filter is %s.";
   public static final String EXCEPTION_NULL_9B41EF67 = "null";
   public static final String EXCEPTION_ARG_MUST_HAVE_JOIN_KEYS_DOT_C24DAB2D = 
"%s must have join keys.";
diff --git 
a/iotdb-core/calc-commons/src/main/i18n/zh/org/apache/iotdb/calc/i18n/CalcMessages.java
 
b/iotdb-core/calc-commons/src/main/i18n/zh/org/apache/iotdb/calc/i18n/CalcMessages.java
index 20c689943a6..817d8376e60 100644
--- 
a/iotdb-core/calc-commons/src/main/i18n/zh/org/apache/iotdb/calc/i18n/CalcMessages.java
+++ 
b/iotdb-core/calc-commons/src/main/i18n/zh/org/apache/iotdb/calc/i18n/CalcMessages.java
@@ -382,6 +382,8 @@ public final class CalcMessages {
   public static final String 
EXCEPTION_THE_SIZE_OF_READYQUEUE_CANNOT_BE_NEGATIVE_DOT_01D8D0CB = "readyQueue 
的大小不能为负数。";
   public static final String 
EXCEPTION_THE_SIZE_BETWEEN_WHENTRANSFORMERS_AND_THENTRANSFORMERS_NEEDS_TO_BE_SAME_AC796883
 = "whenTransformers 和 thenTransformers 的大小应相同";
   public static final String 
EXCEPTION_EXCEED_MAX_CALL_TIMES_OF_GETCOLUMN_69C77C7E = "超过 getColumn 的最大调用次数";
+  public static final String 
EXCEPTION_TOPK_RUNTIME_FILTER_REQUIRES_SINGLE_TIME_ORDER_BY_7EED6208 =
+      "TopK Runtime Filter 要求 ORDER BY 单个 INT64 或 TIMESTAMP 类型的 time 列";
   public static final String 
EXCEPTION_FILTER_IS_NOT_SUPPORTED_IN_ARG_DOT_FILTER_IS_ARG_DOT_417C4F3C = "%s 
不支持过滤条件。过滤条件为 %s。";
   public static final String EXCEPTION_NULL_9B41EF67 = "null";
   public static final String EXCEPTION_ARG_MUST_HAVE_JOIN_KEYS_DOT_C24DAB2D = 
"%s 必须包含连接键。";
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilter.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilter.java
new file mode 100644
index 00000000000..52d3ccbcaf6
--- /dev/null
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilter.java
@@ -0,0 +1,67 @@
+/*
+ * 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.filter;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A dynamic time threshold shared between TopK and Scan operators.
+ *
+ * <p>When TopK heap is full, the worst row in the heap becomes the threshold. 
Scan operators can
+ * skip rows (and even files) that cannot possibly enter the final TopK result.
+ */
+public class TopKRuntimeFilter {
+  private final boolean ascending;
+  private final AtomicLong threshold;
+
+  public TopKRuntimeFilter(boolean ascending) {
+    this.ascending = ascending;
+    threshold = new AtomicLong(ascending ? Long.MAX_VALUE : Long.MIN_VALUE);
+  }
+
+  public boolean isAscending() {
+    return ascending;
+  }
+
+  /** Update threshold with the current heap-top time. Only tightens the 
bound. */
+  public void updateThreshold(long time) {
+    if (ascending) {
+      // ASC TopK: keep smallest K rows, threshold is the largest time among 
them
+      threshold.updateAndGet(prev -> Math.min(prev, time));
+    } else {
+      // DESC TopK: keep largest K rows, threshold is the smallest time among 
them
+      threshold.updateAndGet(prev -> Math.max(prev, time));
+    }
+  }
+
+  public boolean mayQualify(long time) {
+    long current = threshold.get();
+    return ascending ? time < current : time > current;
+  }
+
+  public boolean mayQualifyRange(long startTime, long endTime) {
+    long current = threshold.get();
+    return ascending ? startTime < current : endTime > current;
+  }
+
+  public long getThreshold() {
+    return threshold.get();
+  }
+}
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableTopKOperator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableTopKOperator.java
index 2a7d11657b7..872860d75d1 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableTopKOperator.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TableTopKOperator.java
@@ -19,9 +19,11 @@
 
 package org.apache.iotdb.calc.execution.operator.process;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
 import org.apache.iotdb.calc.execution.operator.Operator;
 import org.apache.iotdb.calc.plan.planner.CommonOperatorUtils;
+import org.apache.iotdb.calc.utils.datastructure.MergeSortKey;
 import org.apache.iotdb.calc.utils.datastructure.SortKey;
 
 import org.apache.tsfile.block.column.Column;
@@ -33,6 +35,29 @@ import java.util.Comparator;
 import java.util.List;
 
 public class TableTopKOperator extends TopKOperator {
+
+  private final int timeFilterColumnIndex;
+
+  public TableTopKOperator(
+      CommonOperatorContext operatorContext,
+      List<Operator> childrenOperators,
+      List<TSDataType> dataTypes,
+      Comparator<SortKey> comparator,
+      int topValue,
+      boolean childrenDataInOrder,
+      TopKRuntimeFilter topKRuntimeFilter,
+      int timeFilterColumnIndex) {
+    super(
+        operatorContext,
+        childrenOperators,
+        dataTypes,
+        comparator,
+        topValue,
+        childrenDataInOrder,
+        topKRuntimeFilter);
+    this.timeFilterColumnIndex = timeFilterColumnIndex;
+  }
+
   public TableTopKOperator(
       CommonOperatorContext operatorContext,
       List<Operator> childrenOperators,
@@ -40,7 +65,20 @@ public class TableTopKOperator extends TopKOperator {
       Comparator<SortKey> comparator,
       int topValue,
       boolean childrenDataInOrder) {
-    super(operatorContext, childrenOperators, dataTypes, comparator, topValue, 
childrenDataInOrder);
+    super(
+        operatorContext,
+        childrenOperators,
+        dataTypes,
+        comparator,
+        topValue,
+        childrenDataInOrder,
+        null);
+    this.timeFilterColumnIndex = -1;
+  }
+
+  @Override
+  protected long extractThresholdTime(MergeSortKey peek) {
+    return 
peek.tsBlock.getColumn(timeFilterColumnIndex).getLong(peek.rowIndex);
   }
 
   @Override
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TopKOperator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TopKOperator.java
index 53ceeee58a0..b2754cbbdd6 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TopKOperator.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/TopKOperator.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.calc.execution.operator.process;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
 import org.apache.iotdb.calc.execution.operator.Operator;
 import org.apache.iotdb.calc.i18n.CalcMessages;
@@ -85,6 +86,8 @@ public abstract class TopKOperator implements ProcessOperator 
{
   // the data of every childOperator is in order
   private final boolean childrenDataInOrder;
 
+  private final TopKRuntimeFilter topKRuntimeFilter;
+
   public static final int OPERATOR_BATCH_UPPER_BOUND = 100000;
 
   protected TopKOperator(
@@ -94,6 +97,24 @@ public abstract class TopKOperator implements 
ProcessOperator {
       Comparator<SortKey> comparator,
       int topValue,
       boolean childrenDataInOrder) {
+    this(
+        operatorContext,
+        childrenOperators,
+        dataTypes,
+        comparator,
+        topValue,
+        childrenDataInOrder,
+        null);
+  }
+
+  protected TopKOperator(
+      CommonOperatorContext operatorContext,
+      List<Operator> childrenOperators,
+      List<TSDataType> dataTypes,
+      Comparator<SortKey> comparator,
+      int topValue,
+      boolean childrenDataInOrder,
+      TopKRuntimeFilter topKRuntimeFilter) {
     this.operatorContext = operatorContext;
     this.childrenOperators = childrenOperators;
     this.dataTypes = dataTypes;
@@ -102,6 +123,7 @@ public abstract class TopKOperator implements 
ProcessOperator {
     this.tsBlockBuilder = new TsBlockBuilder(topValue, dataTypes);
     this.topValue = topValue;
     this.childrenDataInOrder = childrenDataInOrder;
+    this.topKRuntimeFilter = topKRuntimeFilter;
 
     initResultTsBlock();
 
@@ -209,6 +231,7 @@ public abstract class TopKOperator implements 
ProcessOperator {
       if (skipCurrentBatch) {
         closeOperator(i);
       }
+      updateTopKRuntimeFilter();
       canCallNext[i] = false;
 
       if (System.nanoTime() - startTime > maxRuntime) {
@@ -226,6 +249,19 @@ public abstract class TopKOperator implements 
ProcessOperator {
     return null;
   }
 
+  private void updateTopKRuntimeFilter() {
+    if (topKRuntimeFilter == null || mergeSortHeap.getHeapSize() < topValue) {
+      return;
+    }
+    MergeSortKey peek = mergeSortHeap.peek();
+    topKRuntimeFilter.updateThreshold(extractThresholdTime(peek));
+  }
+
+  /** Table model overrides to read the time field. */
+  protected long extractThresholdTime(MergeSortKey peek) {
+    return peek.tsBlock.getTimeByIndex(peek.rowIndex);
+  }
+
   @Override
   public void close() throws Exception {
     for (int i = childIndex; i < childrenOperators.size(); i++) {
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 5a003e18d3c..a2782ac0798 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
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.calc.plan.planner;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
 import org.apache.iotdb.calc.execution.operator.Operator;
 import org.apache.iotdb.calc.execution.operator.process.AssignUniqueIdOperator;
@@ -888,6 +889,7 @@ public abstract class TableOperatorGenerator<
   public Operator visitTopK(TopKNode node, C context) {
     CommonOperatorContext operatorContext =
         addOperatorContext(context, node.getPlanNodeId(), 
TableTopKOperator.class.getSimpleName());
+    TopKRuntimeFilter topKRuntimeFilter = registerRuntimeFilter(context, node);
     List<Operator> children = new ArrayList<>(node.getChildren().size());
     for (PlanNode child : node.getChildren()) {
       children.add(this.process(child, context));
@@ -903,6 +905,25 @@ public abstract class TableOperatorGenerator<
         sortItemIndexList,
         sortItemDataTypeList,
         context.getTableTypeProvider());
+    if (topKRuntimeFilter != null) {
+      checkArgument(
+          sortItemsCount == 1,
+          
CalcMessages.EXCEPTION_TOPK_RUNTIME_FILTER_REQUIRES_SINGLE_TIME_ORDER_BY_7EED6208);
+      TSDataType sortType = sortItemDataTypeList.get(0);
+      checkArgument(
+          sortType == TSDataType.INT64 || sortType == TSDataType.TIMESTAMP,
+          
CalcMessages.EXCEPTION_TOPK_RUNTIME_FILTER_REQUIRES_SINGLE_TIME_ORDER_BY_7EED6208);
+      return new TableTopKOperator(
+          operatorContext,
+          children,
+          dataTypes,
+          getComparatorForTable(
+              node.getOrderingScheme().getOrderingList(), sortItemIndexList, 
sortItemDataTypeList),
+          (int) node.getCount(),
+          node.isChildrenDataInOrder(),
+          topKRuntimeFilter,
+          sortItemIndexList.get(0));
+    }
     return new TableTopKOperator(
         operatorContext,
         children,
@@ -913,6 +934,10 @@ public abstract class TableOperatorGenerator<
         node.isChildrenDataInOrder());
   }
 
+  protected TopKRuntimeFilter registerRuntimeFilter(C context, TopKNode node) {
+    return null;
+  }
+
   protected List<TSDataType> getOutputColumnTypes(PlanNode node, 
ITableTypeProvider typeProvider) {
     return node.getOutputSymbols().stream()
         .map(s -> getTSDataType(typeProvider.getTableModelType(s)))
diff --git 
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilterTest.java
 
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilterTest.java
new file mode 100644
index 00000000000..b84db8bb4a6
--- /dev/null
+++ 
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/filter/TopKRuntimeFilterTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.filter;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TopKRuntimeFilterTest {
+
+  @Test
+  public void testDescRuntimeFilter() {
+    TopKRuntimeFilter filter = new TopKRuntimeFilter(false);
+    Assert.assertTrue(filter.mayQualify(100L));
+    Assert.assertTrue(filter.mayQualifyRange(0L, Long.MAX_VALUE));
+
+    filter.updateThreshold(50L);
+    Assert.assertTrue(filter.mayQualify(60L));
+    Assert.assertFalse(filter.mayQualify(50L));
+    Assert.assertFalse(filter.mayQualify(40L));
+
+    filter.updateThreshold(55L);
+    Assert.assertFalse(filter.mayQualify(54L));
+    Assert.assertFalse(filter.mayQualify(55L));
+    Assert.assertTrue(filter.mayQualify(56L));
+  }
+
+  @Test
+  public void testAscRuntimeFilter() {
+    TopKRuntimeFilter filter = new TopKRuntimeFilter(true);
+    Assert.assertTrue(filter.mayQualify(100L));
+    Assert.assertTrue(filter.mayQualifyRange(0L, Long.MAX_VALUE - 1));
+
+    filter.updateThreshold(100L);
+    Assert.assertTrue(filter.mayQualify(90L));
+    Assert.assertFalse(filter.mayQualify(100L));
+    Assert.assertFalse(filter.mayQualify(110L));
+
+    filter.updateThreshold(80L);
+    Assert.assertFalse(filter.mayQualify(90L));
+    Assert.assertTrue(filter.mayQualify(70L));
+  }
+
+  @Test
+  public void testMayQualifyRange() {
+    TopKRuntimeFilter filter = new TopKRuntimeFilter(false);
+    filter.updateThreshold(50L);
+    Assert.assertTrue(filter.mayQualifyRange(60L, 100L));
+    Assert.assertFalse(filter.mayQualifyRange(50L, 50L));
+    Assert.assertFalse(filter.mayQualifyRange(10L, 40L));
+
+    TopKRuntimeFilter ascFilter = new TopKRuntimeFilter(true);
+    ascFilter.updateThreshold(100L);
+    Assert.assertTrue(ascFilter.mayQualifyRange(10L, 99L));
+    Assert.assertFalse(ascFilter.mayQualifyRange(100L, 200L));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
index 7f151e450d2..cf901ebd2db 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
@@ -582,6 +582,12 @@ public class IoTDBConfig {
 
   private volatile boolean enableTsFileValidation = false;
 
+  /**
+   * Whether to enable the TopK runtime filter optimization for table-model 
{@code ORDER BY time
+   * LIMIT k} queries. Hot-reloadable; set false to fall back to the original 
execution path.
+   */
+  private volatile boolean enableTopKRuntimeFilter = true;
+
   /** The size of candidate compaction task queue. */
   private int candidateCompactionTaskQueueSize = 50;
 
@@ -4418,6 +4424,14 @@ public class IoTDBConfig {
     this.enableTsFileValidation = enableTsFileValidation;
   }
 
+  public boolean isEnableTopKRuntimeFilter() {
+    return enableTopKRuntimeFilter;
+  }
+
+  public void setEnableTopKRuntimeFilter(boolean enableTopKRuntimeFilter) {
+    this.enableTopKRuntimeFilter = enableTopKRuntimeFilter;
+  }
+
   public long getInnerCompactionTaskSelectionModsFileThreshold() {
     return innerCompactionTaskSelectionModsFileThreshold;
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
index e3862bc0e9b..1ab0c22bbf6 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
@@ -707,6 +707,11 @@ public class IoTDBDescriptor {
             properties.getProperty(
                 "enable_tsfile_validation", 
String.valueOf(conf.isEnableTsFileValidation()))));
 
+    conf.setEnableTopKRuntimeFilter(
+        Boolean.parseBoolean(
+            properties.getProperty(
+                "enable_topk_runtime_filter", 
String.valueOf(conf.isEnableTopKRuntimeFilter()))));
+
     conf.setCandidateCompactionTaskQueueSize(
         Integer.parseInt(
             properties.getProperty(
@@ -2216,6 +2221,13 @@ public class IoTDBDescriptor {
                   ConfigurationFileUtils.getConfigurationDefaultValue(
                       "enable_tsfile_validation"))));
 
+      conf.setEnableTopKRuntimeFilter(
+          Boolean.parseBoolean(
+              properties.getProperty(
+                  "enable_topk_runtime_filter",
+                  ConfigurationFileUtils.getConfigurationDefaultValue(
+                      "enable_topk_runtime_filter"))));
+
       // update wal config
       long prevDeleteWalFilesPeriodInMs = conf.getDeleteWalFilesPeriodInMs();
       loadWALHotModifiedProps(properties);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/DataNodeQueryContext.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/DataNodeQueryContext.java
index ef6b60eec4d..3e3b8beee01 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/DataNodeQueryContext.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/DataNodeQueryContext.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.execution.fragment;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.commons.client.exception.ClientManagerException;
 import org.apache.iotdb.commons.path.PartialPath;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.QualifiedObjectName;
@@ -75,6 +76,26 @@ public class DataNodeQueryContext {
   // unnecessary
   private final ReentrantLock lock = new ReentrantLock();
 
+  /**
+   * TopK runtime filters shared across fragment instances on this DataNode 
for the same query.
+   *
+   * <p>Key: root TopK plan node id. Value: filter instance produced by the 
TopK operator and
+   * consumed by Scan operators referencing the same id via {@code 
topKRuntimeFilterSourceId}.
+   */
+  private final Map<String, TopKRuntimeFilter> runtimeFilters = new 
ConcurrentHashMap<>();
+
+  /**
+   * Registers a TopK runtime filter for the given root TopK id. If multiple 
fragment instances bind
+   * the same id, they share one filter instance.
+   */
+  public TopKRuntimeFilter registerTopKRuntimeFilter(String filterId, 
TopKRuntimeFilter filter) {
+    return runtimeFilters.computeIfAbsent(filterId, id -> filter);
+  }
+
+  public TopKRuntimeFilter getTopKRuntimeFilter(String filterId) {
+    return runtimeFilters.get(filterId);
+  }
+
   public DataNodeQueryContext(int dataNodeFINum) {
     this.uncachedPathToSeriesScanInfo = new ConcurrentHashMap<>();
     this.dataNodeFINum = new AtomicInteger(dataNodeFINum);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
index c6a440b1884..52ddf505a5e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.execution.operator.source;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.commons.path.IFullPath;
 import org.apache.iotdb.commons.path.NonAlignedFullPath;
 import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
@@ -139,6 +140,7 @@ public class SeriesScanUtil implements Accountable {
 
   protected SeriesScanOptions scanOptions;
   private final PaginationController paginationController;
+  private boolean runtimeFilterExhausted;
 
   private static final SeriesScanCostMetricSet SERIES_SCAN_COST_METRIC_SET =
       SeriesScanCostMetricSet.getInstance();
@@ -205,6 +207,9 @@ public class SeriesScanUtil implements Accountable {
    * @param dataSource the query data source
    */
   public void initQueryDataSource(QueryDataSource dataSource) {
+    if (scanOptions.getTopKRuntimeFilter() != null) {
+      dataSource.initRuntimeFilterTracking();
+    }
     dataSource.fillOrderIndexes(deviceID, orderUtils.getAscending());
     this.dataSource = dataSource;
 
@@ -276,7 +281,7 @@ public class SeriesScanUtil implements Accountable {
   // Optional.empty(), it needs to return directly to the checkpoint method 
that checks the operator
   // execution time slice.
   public Optional<Boolean> hasNextFile() throws IOException {
-    if (!paginationController.hasCurLimit()) {
+    if (runtimeFilterExhausted || !paginationController.hasCurLimit()) {
       return Optional.of(false);
     }
 
@@ -339,6 +344,27 @@ public class SeriesScanUtil implements Accountable {
         && filterAllSatisfy(scanOptions.getPushDownFilter(), 
firstTimeSeriesMetadata);
   }
 
+  /**
+   * Returns false when the time range cannot contain any row that may still 
qualify for TopK, so
+   * the caller can skip decoding the whole file/chunk/page.
+   */
+  private boolean mayQualifyRuntimeFilterRange(Statistics<? extends 
Serializable> statistics) {
+    TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
+    if (filter == null) {
+      return true;
+    }
+    return filter.mayQualifyRange(statistics.getStartTime(), 
statistics.getEndTime());
+  }
+
+  private boolean skipByTopKRuntimeFilter(
+      Statistics<? extends Serializable> statistics, Runnable skip) {
+    if (!mayQualifyRuntimeFilterRange(statistics)) {
+      skip.run();
+      return true;
+    }
+    return false;
+  }
+
   @SuppressWarnings("squid:S3740")
   public Statistics currentFileTimeStatistics() {
     return firstTimeSeriesMetadata.getTimeStatistics();
@@ -369,7 +395,7 @@ public class SeriesScanUtil implements Accountable {
    * @throws IllegalStateException illegal state
    */
   public Optional<Boolean> hasNextChunk() throws IOException {
-    if (!paginationController.hasCurLimit()) {
+    if (runtimeFilterExhausted || !paginationController.hasCurLimit()) {
       return Optional.of(false);
     }
 
@@ -419,6 +445,10 @@ public class SeriesScanUtil implements Accountable {
       return;
     }
 
+    if (skipByTopKRuntimeFilter(firstChunkMetadata.getStatistics(), 
this::skipCurrentChunk)) {
+      return;
+    }
+
     // globalTimeFilter.canSkip() must be FALSE
     Filter pushDownFilter = scanOptions.getPushDownFilter();
     if (pushDownFilter != null && pushDownFilter.canSkip(firstChunkMetadata)) {
@@ -542,7 +572,7 @@ public class SeriesScanUtil implements Accountable {
   @SuppressWarnings("squid:S3776")
   // Suppress high Cognitive Complexity warning
   public boolean hasNextPage() throws IOException {
-    if (!paginationController.hasCurLimit()) {
+    if (runtimeFilterExhausted || !paginationController.hasCurLimit()) {
       return false;
     }
 
@@ -919,6 +949,10 @@ public class SeriesScanUtil implements Accountable {
   }
 
   private TsBlock filterAndPaginateCachedBlock(TsBlock tsBlock) {
+    tsBlock = applyRuntimeFilterToTsBlock(tsBlock);
+    if (tsBlock == null || tsBlock.isEmpty()) {
+      return null;
+    }
     if (scanOptions.getPushDownFilter() == null) {
       return paginationController.applyTsBlock(tsBlock);
     }
@@ -937,6 +971,31 @@ public class SeriesScanUtil implements Accountable {
         paginationController);
   }
 
+  private TsBlock applyRuntimeFilterToTsBlock(TsBlock tsBlock) {
+    TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
+    if (filter == null) {
+      return tsBlock;
+    }
+
+    int positionCount = tsBlock.getPositionCount();
+    int keepCount = positionCount;
+    for (int i = 0; i < positionCount; i++) {
+      if (!filter.mayQualify(tsBlock.getTimeByIndex(i))) {
+        keepCount = i;
+        runtimeFilterExhausted = true;
+        break;
+      }
+    }
+
+    if (keepCount == positionCount) {
+      return tsBlock;
+    }
+    if (keepCount == 0) {
+      return null;
+    }
+    return tsBlock.getRegion(0, keepCount);
+  }
+
   private TsBlock getTransferedDataTypeTsBlock(TsBlock tsBlock) {
     Column[] valueColumns = tsBlock.getValueColumns();
     int length = tsBlock.getValueColumnCount();
@@ -1406,6 +1465,10 @@ public class SeriesScanUtil implements Accountable {
       return;
     }
 
+    if (skipByTopKRuntimeFilter(firstPageReader.getStatistics(), 
this::skipCurrentPage)) {
+      return;
+    }
+
     IPageReader pageReader = firstPageReader.getPageReader();
 
     // globalTimeFilter.canSkip() must be FALSE
@@ -1892,6 +1955,10 @@ public class SeriesScanUtil implements Accountable {
       return;
     }
 
+    if (skipByTopKRuntimeFilter(firstTimeSeriesMetadata.getStatistics(), 
this::skipCurrentFile)) {
+      return;
+    }
+
     // globalTimeFilter.canSkip() must be FALSE
     Filter pushDownFilter = scanOptions.getPushDownFilter();
     if (pushDownFilter != null && 
pushDownFilter.canSkip(firstTimeSeriesMetadata)) {
@@ -2393,10 +2460,23 @@ public class SeriesScanUtil implements Accountable {
 
     @Override
     public boolean hasNextSeqResource() {
+      TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
       while (dataSource.hasNextSeqResource(curSeqFileIndex, false, deviceID)) {
+        if (filter != null && dataSource.isRuntimeFilterPruned(true, 
curSeqFileIndex)) {
+          curSeqFileIndex--;
+          continue;
+        }
         if (dataSource.isSeqSatisfied(
             deviceID, curSeqFileIndex, scanOptions.getGlobalTimeFilter(), 
false)) {
-          break;
+          if (filter == null
+              || dataSource.isSeqSatisfiedByRuntimeFilter(curSeqFileIndex, 
filter, false)) {
+            break;
+          }
+          dataSource.setSeqTsFileResourceInvalidated(curSeqFileIndex);
+          if (!dataSource.hasValidResource()) {
+            runtimeFilterExhausted = true;
+            return false;
+          }
         }
         curSeqFileIndex--;
       }
@@ -2405,10 +2485,23 @@ public class SeriesScanUtil implements Accountable {
 
     @Override
     public boolean hasNextUnseqResource() {
+      TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
       while (dataSource.hasNextUnseqResource(curUnseqFileIndex, false, 
deviceID)) {
+        if (filter != null && dataSource.isRuntimeFilterPruned(false, 
curUnseqFileIndex)) {
+          curUnseqFileIndex++;
+          continue;
+        }
         if (dataSource.isUnSeqSatisfied(
             deviceID, curUnseqFileIndex, scanOptions.getGlobalTimeFilter(), 
false)) {
-          break;
+          if (filter == null
+              || dataSource.isUnSeqSatisfiedByRuntimeFilter(curUnseqFileIndex, 
filter)) {
+            break;
+          }
+          dataSource.setUnseqTsFileResourceInvalidated(curUnseqFileIndex);
+          if (!dataSource.hasValidResource()) {
+            runtimeFilterExhausted = true;
+            return false;
+          }
         }
         curUnseqFileIndex++;
       }
@@ -2522,10 +2615,23 @@ public class SeriesScanUtil implements Accountable {
 
     @Override
     public boolean hasNextSeqResource() {
+      TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
       while (dataSource.hasNextSeqResource(curSeqFileIndex, true, deviceID)) {
+        if (filter != null && dataSource.isRuntimeFilterPruned(true, 
curSeqFileIndex)) {
+          curSeqFileIndex++;
+          continue;
+        }
         if (dataSource.isSeqSatisfied(
             deviceID, curSeqFileIndex, scanOptions.getGlobalTimeFilter(), 
false)) {
-          break;
+          if (filter == null
+              || dataSource.isSeqSatisfiedByRuntimeFilter(curSeqFileIndex, 
filter, false)) {
+            break;
+          }
+          dataSource.setSeqTsFileResourceInvalidated(curSeqFileIndex);
+          if (!dataSource.hasValidResource()) {
+            runtimeFilterExhausted = true;
+            return false;
+          }
         }
         curSeqFileIndex++;
       }
@@ -2534,10 +2640,23 @@ public class SeriesScanUtil implements Accountable {
 
     @Override
     public boolean hasNextUnseqResource() {
+      TopKRuntimeFilter filter = scanOptions.getTopKRuntimeFilter();
       while (dataSource.hasNextUnseqResource(curUnseqFileIndex, true, 
deviceID)) {
+        if (filter != null && dataSource.isRuntimeFilterPruned(false, 
curUnseqFileIndex)) {
+          curUnseqFileIndex++;
+          continue;
+        }
         if (dataSource.isUnSeqSatisfied(
             deviceID, curUnseqFileIndex, scanOptions.getGlobalTimeFilter(), 
false)) {
-          break;
+          if (filter == null
+              || dataSource.isUnSeqSatisfiedByRuntimeFilter(curUnseqFileIndex, 
filter)) {
+            break;
+          }
+          dataSource.setUnseqTsFileResourceInvalidated(curUnseqFileIndex);
+          if (!dataSource.hasValidResource()) {
+            runtimeFilterExhausted = true;
+            return false;
+          }
         }
         curUnseqFileIndex++;
       }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/AbstractTableScanOperator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/AbstractTableScanOperator.java
index 824d46d3a02..6e9794dcf82 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/AbstractTableScanOperator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/AbstractTableScanOperator.java
@@ -82,7 +82,7 @@ public abstract class AbstractTableScanOperator extends 
AbstractSeriesScanOperat
 
   private TsBlock measurementDataBlock;
 
-  private QueryDataSource queryDataSource;
+  protected QueryDataSource queryDataSource;
 
   protected int currentDeviceIndex;
 
@@ -217,8 +217,16 @@ public abstract class AbstractTableScanOperator extends 
AbstractSeriesScanOperat
 
   @Override
   public boolean isFinished() throws Exception {
-    return (retainedTsBlock == null)
-        && (currentDeviceIndex >= deviceCount || 
seriesScanOptions.limitConsumedUp());
+    if (retainedTsBlock != null) {
+      return false;
+    }
+    if (seriesScanOptions.limitConsumedUp()) {
+      return true;
+    }
+    if (currentDeviceIndex >= deviceCount) {
+      return true;
+    }
+    return shouldStopScanByRuntimeFilter();
   }
 
   @Override
@@ -251,6 +259,10 @@ public abstract class AbstractTableScanOperator extends 
AbstractSeriesScanOperat
   }
 
   protected void moveToNextDevice() {
+    if (shouldStopScanByRuntimeFilter()) {
+      currentDeviceIndex = deviceCount;
+      return;
+    }
     currentDeviceIndex++;
     if (currentDeviceIndex < deviceCount) {
       // construct AlignedSeriesScanUtil for next device
@@ -264,6 +276,11 @@ public abstract class AbstractTableScanOperator extends 
AbstractSeriesScanOperat
     }
   }
 
+  /** Returns true when file-level RF has pruned all seq/unseq files — scan 
can stop globally. */
+  protected boolean shouldStopScanByRuntimeFilter() {
+    return seriesScanOptions.getTopKRuntimeFilter() != null && 
!queryDataSource.hasValidResource();
+  }
+
   protected void constructAlignedSeriesScanUtil() {
     if (this.deviceEntries.isEmpty() || currentDeviceIndex >= deviceCount) {
       // no need to construct SeriesScanUtil, hasNext will return false
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperator.java
index b11e38f9008..a38fcacd046 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperator.java
@@ -97,12 +97,23 @@ public class ExternalTsFileTableScanOperator extends 
TableScanOperator {
     super.initQueryDataSource(currentDataSource);
   }
 
+  @Override
+  protected boolean shouldStopScanByRuntimeFilter() {
+    // Each device uses its own QueryDataSource; exhausting files for a 
non-last device must not
+    // stop scanning remaining devices in the same external TsFile task.
+    if (currentDeviceIndex < deviceCount - 1) {
+      return false;
+    }
+    return super.shouldStopScanByRuntimeFilter();
+  }
+
   @Override
   protected void moveToNextDevice() {
     currentDeviceIndex++;
     if (currentDeviceIndex < deviceCount) {
       constructAlignedSeriesScanUtil();
-      seriesScanUtil.initQueryDataSource(updateCurrentDeviceQueryDataSource());
+      queryDataSource = updateCurrentDeviceQueryDataSource();
+      seriesScanUtil.initQueryDataSource(queryDataSource);
       this.operatorContext.recordSpecifiedInfo(
           CommonOperatorUtils.CURRENT_DEVICE_INDEX_STRING, 
Integer.toString(currentDeviceIndex));
     }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/DataNodeTableOperatorGenerator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/DataNodeTableOperatorGenerator.java
index a17726d9dc2..e09d05066a3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/DataNodeTableOperatorGenerator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/DataNodeTableOperatorGenerator.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.plan.planner;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
 import org.apache.iotdb.calc.execution.operator.Operator;
 import 
org.apache.iotdb.calc.execution.operator.process.FilterAndProjectOperator;
@@ -46,6 +47,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchem
 import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.QualifiedObjectName;
 import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.AggregationNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.FunctionCall;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.LongLiteral;
@@ -222,6 +224,53 @@ public class DataNodeTableOperatorGenerator
     super(metadata);
   }
 
+  /**
+   * Producer side: register a shared {@link TopKRuntimeFilter} before 
visiting TopK children so
+   * nested Scan nodes can resolve the same instance during operator 
generation.
+   */
+  private TopKRuntimeFilter registerTopKRuntimeFilterForTopK(
+      TopKNode node, LocalExecutionPlanContext context) {
+    if (node.getTopKRuntimeFilterSourceId() == null) {
+      return null;
+    }
+    return context.dataNodeQueryContext.registerTopKRuntimeFilter(
+        node.getTopKRuntimeFilterSourceId(),
+        new TopKRuntimeFilter(node.isTopKRuntimeFilterAscending()));
+  }
+
+  /**
+   * Consumer side: lookup the shared filter by root TopK plan node id ({@code
+   * topKRuntimeFilterSourceId}).
+   */
+  private TopKRuntimeFilter resolveTopKRuntimeFilterForDeviceScan(
+      DeviceTableScanNode scanNode, LocalExecutionPlanContext context) {
+    if (scanNode.getTopKRuntimeFilterSourceId() == null) {
+      return null;
+    }
+    return context.dataNodeQueryContext.getTopKRuntimeFilter(
+        scanNode.getTopKRuntimeFilterSourceId());
+  }
+
+  @Override
+  protected TopKRuntimeFilter registerRuntimeFilter(
+      LocalExecutionPlanContext context, TopKNode node) {
+    return registerTopKRuntimeFilterForTopK(node, context);
+  }
+
+  private void applyTopKRuntimeFilter(
+      SeriesScanOptions.Builder builder,
+      DeviceTableScanNode scanNode,
+      LocalExecutionPlanContext context,
+      TopKRuntimeFilter preResolvedFilter) {
+    TopKRuntimeFilter filter =
+        preResolvedFilter != null
+            ? preResolvedFilter
+            : resolveTopKRuntimeFilterForDeviceScan(scanNode, context);
+    if (filter != null) {
+      builder.withTopKRuntimeFilter(filter);
+    }
+  }
+
   @Override
   protected String getSortTmpDir(CommonOperatorContext operatorContext) {
     OperatorContext dataNodeOperatorContext = (OperatorContext) 
operatorContext;
@@ -626,6 +675,7 @@ public class DataNodeTableOperatorGenerator
                 builder.withPushDownOffset(
                     node.isPushLimitToEachDevice() ? 0 : 
node.getPushDownOffset());
               }
+              applyTopKRuntimeFilter(builder, node, context, null);
               SeriesScanOptions options = builder.build();
               options.setTTLForTableView(viewTTL);
               seriesScanOptionsList.add(options);
@@ -973,13 +1023,15 @@ public class DataNodeTableOperatorGenerator
     IDeviceID.TreeDeviceIdColumnValueExtractor idColumnValueExtractor =
         
createTreeDeviceIdColumnValueExtractor(DataNodeTreeViewSchemaUtils.getPrefixPath(tsTable));
 
+    TopKRuntimeFilter topKRuntimeFilter = 
resolveTopKRuntimeFilterForDeviceScan(node, context);
     AbstractTableScanOperator.AbstractTableScanOperatorParameter parameter =
         constructAbstractTableScanOperatorParameter(
             node,
             context,
             TreeAlignedDeviceViewScanOperator.class.getSimpleName(),
             node.getMeasurementColumnNameMap(),
-            tsTable.getCachedTableTTL());
+            tsTable.getCachedTableTTL(),
+            topKRuntimeFilter);
 
     TreeAlignedDeviceViewScanOperator treeAlignedDeviceViewScanOperator =
         new TreeAlignedDeviceViewScanOperator(parameter, 
idColumnValueExtractor);
@@ -1091,16 +1143,78 @@ public class DataNodeTableOperatorGenerator
         maxTsBlockLineNum);
   }
 
+  private AbstractTableScanOperator.AbstractTableScanOperatorParameter
+      constructAbstractTableScanOperatorParameter(
+          DeviceTableScanNode node,
+          LocalExecutionPlanContext context,
+          String className,
+          Map<String, String> fieldColumnsRenameMap,
+          long viewTTL,
+          TopKRuntimeFilter topKRuntimeFilter) {
+
+    CommonTableScanOperatorParameters commonParameter =
+        new CommonTableScanOperatorParameters(node, fieldColumnsRenameMap, 
false);
+    List<IMeasurementSchema> measurementSchemas = 
commonParameter.measurementSchemas;
+    List<String> measurementColumnNames = 
commonParameter.measurementColumnNames;
+    List<ColumnSchema> columnSchemas = commonParameter.columnSchemas;
+    int[] columnsIndexArray = commonParameter.columnsIndexArray;
+    SeriesScanOptions seriesScanOptions =
+        buildSeriesScanOptions(
+            context,
+            commonParameter.columnSchemaMap,
+            measurementColumnNames,
+            commonParameter.measurementColumnsIndexMap,
+            commonParameter.timeColumnName,
+            node.getTimePredicate(),
+            node.getPushDownLimit(),
+            node.getPushDownOffset(),
+            node.isPushLimitToEachDevice(),
+            node.getPushDownPredicate(),
+            node,
+            topKRuntimeFilter);
+    seriesScanOptions.setTTLForTableView(viewTTL);
+    seriesScanOptions.setIsTableViewForTreeModel(node instanceof 
TreeDeviceViewScanNode);
+
+    OperatorContext operatorContext = addOperatorContext(context, 
node.getPlanNodeId(), className);
+
+    int maxTsBlockLineNum = 
TSFileDescriptor.getInstance().getConfig().getMaxTsBlockLineNumber();
+    if (context.getTypeProvider().getTemplatedInfo() != null) {
+      maxTsBlockLineNum =
+          (int)
+              Math.min(
+                  
context.getTypeProvider().getTemplatedInfo().getLimitValue(), 
maxTsBlockLineNum);
+    }
+
+    Set<String> allSensors = new HashSet<>(measurementColumnNames);
+    allSensors.add("");
+
+    return new AbstractTableScanOperator.AbstractTableScanOperatorParameter(
+        allSensors,
+        operatorContext,
+        node.getPlanNodeId(),
+        columnSchemas,
+        columnsIndexArray,
+        node.getDeviceEntries(),
+        node.getScanOrder(),
+        seriesScanOptions,
+        measurementColumnNames,
+        measurementSchemas,
+        maxTsBlockLineNum);
+  }
+
   // used for TableScanOperator
   private AbstractTableScanOperator.AbstractTableScanOperatorParameter
       constructAbstractTableScanOperatorParameter(
-          DeviceTableScanNode node, LocalExecutionPlanContext context) {
+          DeviceTableScanNode node,
+          LocalExecutionPlanContext context,
+          TopKRuntimeFilter topKRuntimeFilter) {
     return constructAbstractTableScanOperatorParameter(
         node,
         context,
         TableScanOperator.class.getSimpleName(),
         Collections.emptyMap(),
-        Long.MAX_VALUE);
+        Long.MAX_VALUE,
+        topKRuntimeFilter);
   }
 
   @Override
@@ -1119,9 +1233,10 @@ public class DataNodeTableOperatorGenerator
   @Override
   public Operator visitDeviceTableScan(
       DeviceTableScanNode node, LocalExecutionPlanContext context) {
+    TopKRuntimeFilter topKRuntimeFilter = 
resolveTopKRuntimeFilterForDeviceScan(node, context);
 
     AbstractTableScanOperator.AbstractTableScanOperatorParameter parameter =
-        constructAbstractTableScanOperatorParameter(node, context);
+        constructAbstractTableScanOperatorParameter(node, context, 
topKRuntimeFilter);
 
     TableScanOperator tableScanOperator = new TableScanOperator(parameter);
 
@@ -1148,13 +1263,15 @@ public class DataNodeTableOperatorGenerator
       return new EmptyDataOperator(operatorContext);
     }
 
+    TopKRuntimeFilter topKRuntimeFilter = 
resolveTopKRuntimeFilterForDeviceScan(node, context);
     AbstractTableScanOperator.AbstractTableScanOperatorParameter parameter =
         constructAbstractTableScanOperatorParameter(
             node,
             context,
             ExternalTsFileTableScanOperator.class.getSimpleName(),
             Collections.emptyMap(),
-            Long.MAX_VALUE);
+            Long.MAX_VALUE,
+            topKRuntimeFilter);
 
     AbstractTableScanOperator externalTsFileTableScanOperator =
         new ExternalTsFileTableScanOperator(parameter, 
node.getDeviceTaskPartitionIndex());
@@ -1932,6 +2049,34 @@ public class DataNodeTableOperatorGenerator
       long pushDownOffset,
       boolean pushLimitToEachDevice,
       Expression pushDownPredicate) {
+    return buildSeriesScanOptions(
+        context,
+        columnSchemaMap,
+        measurementColumnNames,
+        measurementColumnsIndexMap,
+        timeColumnName,
+        timePredicate,
+        pushDownLimit,
+        pushDownOffset,
+        pushLimitToEachDevice,
+        pushDownPredicate,
+        null,
+        null);
+  }
+
+  private SeriesScanOptions buildSeriesScanOptions(
+      LocalExecutionPlanContext context,
+      Map<Symbol, ColumnSchema> columnSchemaMap,
+      List<String> measurementColumnNames,
+      Map<String, Integer> measurementColumnsIndexMap,
+      String timeColumnName,
+      Optional<Expression> timePredicate,
+      long pushDownLimit,
+      long pushDownOffset,
+      boolean pushLimitToEachDevice,
+      Expression pushDownPredicate,
+      DeviceTableScanNode scanNode,
+      TopKRuntimeFilter topKRuntimeFilter) {
     SeriesScanOptions.Builder scanOptionsBuilder =
         timePredicate
             .map(expression -> getSeriesScanOptionsBuilder(context, 
expression))
@@ -1950,6 +2095,9 @@ public class DataNodeTableOperatorGenerator
               context.getZoneId(),
               TimestampPrecisionUtils.currPrecision));
     }
+    if (scanNode != null) {
+      applyTopKRuntimeFilter(scanOptionsBuilder, scanNode, context, 
topKRuntimeFilter);
+    }
     return scanOptionsBuilder.build();
   }
 
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 06aba49200b..f0d2bc134d7 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
@@ -698,6 +698,10 @@ public class PlanGraphPrinter implements 
PlanVisitor<List<String>, PlanGraphPrin
       boxValue.add(
           String.format(
               "PushDownLimitToEachDevice: %s", 
deviceTableScanNode.isPushLimitToEachDevice()));
+      String topKRuntimeFilterSourceId = 
deviceTableScanNode.getTopKRuntimeFilterSourceId();
+      if (topKRuntimeFilterSourceId != null) {
+        boxValue.add(String.format("TOPN OPT: %s", topKRuntimeFilterSourceId));
+      }
     }
 
     boxValue.add(
@@ -1049,6 +1053,9 @@ public class PlanGraphPrinter implements 
PlanVisitor<List<String>, PlanGraphPrin
     boxValue.add(String.format("TopK-%s", node.getPlanNodeId().getId()));
     boxValue.add(String.format("OrderingScheme: %s", 
node.getOrderingScheme()));
     boxValue.add(String.format("Count: %s", node.getCount()));
+    if (node.getTopKRuntimeFilterSourceId() != null) {
+      boxValue.add("TOPN OPT");
+    }
     return render(node, boxValue, context);
   }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/parameter/SeriesScanOptions.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/parameter/SeriesScanOptions.java
index 38335a788f5..e66d4aadc26 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/parameter/SeriesScanOptions.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/parameter/SeriesScanOptions.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.plan.planner.plan.parameter;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
 import org.apache.iotdb.commons.path.AlignedFullPath;
 import org.apache.iotdb.commons.path.IFullPath;
 import org.apache.iotdb.commons.path.NonAlignedFullPath;
@@ -55,6 +56,7 @@ public class SeriesScanOptions implements Accountable {
   private PaginationController paginationController;
   private boolean isTableViewForTreeModel;
   private long ttlForTableView = Long.MAX_VALUE;
+  private final TopKRuntimeFilter topKRuntimeFilter;
   private static final long INSTANCE_SIZE =
       RamUsageEstimator.shallowSizeOfInstance(
           TreeNonAlignedDeviceViewAggregationScanOperator.class);
@@ -66,7 +68,8 @@ public class SeriesScanOptions implements Accountable {
       long pushDownOffset,
       Set<String> allSensors,
       boolean pushLimitToEachDevice,
-      boolean isTableViewForTreeModel) {
+      boolean isTableViewForTreeModel,
+      TopKRuntimeFilter topKRuntimeFilter) {
     this.globalTimeFilter = globalTimeFilter;
     this.originalTimeFilter = globalTimeFilter;
     this.pushDownFilter = pushDownFilter;
@@ -75,6 +78,7 @@ public class SeriesScanOptions implements Accountable {
     this.allSensors = allSensors;
     this.pushLimitToEachDevice = pushLimitToEachDevice;
     this.isTableViewForTreeModel = isTableViewForTreeModel;
+    this.topKRuntimeFilter = topKRuntimeFilter;
   }
 
   public static SeriesScanOptions getDefaultSeriesScanOptions(IFullPath 
seriesPath) {
@@ -186,6 +190,10 @@ public class SeriesScanOptions implements Accountable {
         && (paginationController != null && 
!paginationController.hasCurLimit());
   }
 
+  public TopKRuntimeFilter getTopKRuntimeFilter() {
+    return topKRuntimeFilter;
+  }
+
   public static class Builder {
 
     private Filter globalTimeFilter = null;
@@ -197,6 +205,7 @@ public class SeriesScanOptions implements Accountable {
 
     private boolean pushLimitToEachDevice = true;
     private boolean isTableViewForTreeModel = false;
+    private TopKRuntimeFilter topKRuntimeFilter;
 
     public Builder withGlobalTimeFilter(Filter globalTimeFilter) {
       this.globalTimeFilter = globalTimeFilter;
@@ -228,6 +237,11 @@ public class SeriesScanOptions implements Accountable {
       return this;
     }
 
+    public Builder withTopKRuntimeFilter(TopKRuntimeFilter topKRuntimeFilter) {
+      this.topKRuntimeFilter = topKRuntimeFilter;
+      return this;
+    }
+
     public void withAllSensors(Set<String> allSensors) {
       this.allSensors = allSensors;
     }
@@ -240,7 +254,8 @@ public class SeriesScanOptions implements Accountable {
           pushDownOffset,
           allSensors,
           pushLimitToEachDevice,
-          isTableViewForTreeModel);
+          isTableViewForTreeModel,
+          topKRuntimeFilter);
     }
   }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
index f8c5f193e1a..c731bee31cc 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
@@ -435,12 +435,14 @@ public class TableDistributedPlanGenerator
     }
 
     TopKNode newTopKNode = (TopKNode) node.clone();
+    // root TopK is not a runtime filter producer.
+    newTopKNode.setTopKRuntimeFilterSourceId(null);
     for (PlanNode child : childrenNodes) {
       PlanNode newChild;
       if (canTopKEliminated(node.getOrderingScheme(), node.getCount(), child)) 
{
         newChild = child;
       } else {
-        newChild =
+        TopKNode regionTopK =
             new TopKNode(
                 queryId.genPlanNodeId(),
                 Collections.singletonList(child),
@@ -448,6 +450,8 @@ public class TableDistributedPlanGenerator
                 node.getCount(),
                 node.getOutputSymbols(),
                 node.isChildrenDataInOrder());
+        
regionTopK.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
+        newChild = regionTopK;
       }
       newTopKNode.addChild(newChild);
     }
@@ -779,6 +783,7 @@ public class TableDistributedPlanGenerator
               partition.getPartitionIndex(),
               node.getSchemaFilter());
       splitNode.setRegionReplicaSet(localRegionReplicaSet);
+      
splitNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
       result.add(splitNode);
     }
     sortPropertyContext.ifPresent(
@@ -914,6 +919,7 @@ public class TableDistributedPlanGenerator
                         node.isPushLimitToEachDevice(),
                         node.containsNonAlignedDevice());
                 scanNode.setRegionReplicaSet(regionReplicaSets.get(0));
+                
scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
                 return scanNode;
               });
       deviceTableScanNode.appendDeviceEntry(deviceEntry);
@@ -1000,6 +1006,7 @@ public class TableDistributedPlanGenerator
                           node.isPushLimitToEachDevice(),
                           node.containsNonAlignedDevice());
                   scanNode.setRegionReplicaSet(regionReplicaSet);
+                  
scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
                   return scanNode;
                 });
         deviceTableScanNode.appendDeviceEntry(deviceEntry);
@@ -1094,6 +1101,7 @@ public class TableDistributedPlanGenerator
                   node.getTreeDBName(),
                   node.getMeasurementColumnNameMap());
           scanNode.setRegionReplicaSet(regionReplicaSet);
+          
scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
           pair.left = scanNode;
         }
 
@@ -1116,6 +1124,7 @@ public class TableDistributedPlanGenerator
                   node.getTreeDBName(),
                   node.getMeasurementColumnNameMap());
           scanNode.setRegionReplicaSet(regionReplicaSet);
+          
scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId());
           pair.right = scanNode;
         }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanner.java
index fd9bc35fb4c..867fe4ecf67 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanner.java
@@ -176,7 +176,10 @@ public class TableDistributedPlanner {
         .forEach((k, v) -> 
mppQueryContext.getTypeProvider().putTableModelType(k, v));
 
     // add exchange node for distributed plan
-    return new 
AddExchangeNodes(mppQueryContext).addExchangeNodes(distributedPlan, 
planContext);
+    PlanNode planWithExchange =
+        new 
AddExchangeNodes(mppQueryContext).addExchangeNodes(distributedPlan, 
planContext);
+
+    return planWithExchange;
   }
 
   private DistributedQueryPlan generateDistributedPlan(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/DeviceTableScanNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/DeviceTableScanNode.java
index c02c74058af..3b7b365c329 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/DeviceTableScanNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/DeviceTableScanNode.java
@@ -79,6 +79,9 @@ public class DeviceTableScanNode extends TableScanNode {
 
   protected transient boolean containsNonAlignedDevice;
 
+  // Id of the TopKNode that produces the runtime filter for this scan; set 
during optimize.
+  @Nullable protected String topKRuntimeFilterSourceId;
+
   protected DeviceTableScanNode() {}
 
   public DeviceTableScanNode(
@@ -129,20 +132,23 @@ public class DeviceTableScanNode extends TableScanNode {
 
   @Override
   public DeviceTableScanNode clone() {
-    return new DeviceTableScanNode(
-        getPlanNodeId(),
-        qualifiedObjectName,
-        outputSymbols,
-        assignments,
-        deviceEntries,
-        tagAndAttributeIndexMap,
-        scanOrder,
-        timePredicate,
-        pushDownPredicate,
-        pushDownLimit,
-        pushDownOffset,
-        pushLimitToEachDevice,
-        containsNonAlignedDevice);
+    DeviceTableScanNode cloned =
+        new DeviceTableScanNode(
+            getPlanNodeId(),
+            qualifiedObjectName,
+            outputSymbols,
+            assignments,
+            deviceEntries,
+            tagAndAttributeIndexMap,
+            scanOrder,
+            timePredicate,
+            pushDownPredicate,
+            pushDownLimit,
+            pushDownOffset,
+            pushLimitToEachDevice,
+            containsNonAlignedDevice);
+    cloned.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+    return cloned;
   }
 
   protected static void serializeMemberVariables(
@@ -170,6 +176,8 @@ public class DeviceTableScanNode extends TableScanNode {
     }
 
     ReadWriteIOUtils.write(node.pushLimitToEachDevice, byteBuffer);
+
+    ReadWriteIOUtils.write(node.topKRuntimeFilterSourceId, byteBuffer);
   }
 
   protected static void serializeMemberVariables(
@@ -198,6 +206,8 @@ public class DeviceTableScanNode extends TableScanNode {
     }
 
     ReadWriteIOUtils.write(node.pushLimitToEachDevice, stream);
+
+    ReadWriteIOUtils.write(node.topKRuntimeFilterSourceId, stream);
   }
 
   protected static void deserializeMemberVariables(
@@ -227,6 +237,8 @@ public class DeviceTableScanNode extends TableScanNode {
     }
 
     node.pushLimitToEachDevice = ReadWriteIOUtils.readBool(byteBuffer);
+
+    node.topKRuntimeFilterSourceId = ReadWriteIOUtils.readString(byteBuffer);
   }
 
   @Override
@@ -307,6 +319,15 @@ public class DeviceTableScanNode extends TableScanNode {
     this.containsNonAlignedDevice = true;
   }
 
+  @Nullable
+  public String getTopKRuntimeFilterSourceId() {
+    return topKRuntimeFilterSourceId;
+  }
+
+  public void setTopKRuntimeFilterSourceId(@Nullable String 
topKRuntimeFilterSourceId) {
+    this.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+  }
+
   public String toString() {
     return "DeviceTableScanNode-" + this.getPlanNodeId();
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
index afe68803948..78bb5f984a1 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
@@ -390,6 +390,7 @@ public class LogicalOptimizeFactory {
                 new MergeLimitWithSort(),
                 new MergeLimitOverProjectWithSort(),
                 new PushTopKThroughUnion())),
+        new TopKRuntimeFilterOptimizer(),
         new ParallelizeGrouping());
 
     this.planOptimizers = optimizerBuilder.build();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizer.java
new file mode 100644
index 00000000000..137af685e06
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizer.java
@@ -0,0 +1,83 @@
+/*
+ * 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.optimizations;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.OrderingScheme;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationTableScanNode;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
+
+/**
+ * <b>Optimization phase:</b> Logical plan planning (after {@code 
PushTopKThroughUnion}).
+ *
+ * <p>Marks the {@code TopK + DeviceTableScan} pattern for TopK runtime 
filter. A qualifying TopK
+ * uses its own plan node id as {@code topKRuntimeFilterSourceId}; the same id 
is stamped on direct
+ * raw {@link DeviceTableScanNode} children. Distributed planning later copies 
this id onto
+ * per-region TopK nodes and clears it on the root TopK.
+ */
+public class TopKRuntimeFilterOptimizer implements PlanOptimizer {
+
+  @Override
+  public PlanNode optimize(PlanNode plan, Context context) {
+    if 
(!IoTDBDescriptor.getInstance().getConfig().isEnableTopKRuntimeFilter()) {
+      return plan;
+    }
+    return plan.accept(new Rewriter(), null);
+  }
+
+  private static class Rewriter implements PlanVisitor<PlanNode, Void> {
+
+    @Override
+    public PlanNode visitPlan(PlanNode node, Void unused) {
+      for (PlanNode child : node.getChildren()) {
+        child.accept(this, null);
+      }
+      return node;
+    }
+
+    @Override
+    public PlanNode visitTopK(TopKNode node, Void unused) {
+      String topKId = node.getPlanNodeId().getId();
+      for (PlanNode child : node.getChildren()) {
+        boolean isRawDeviceTableScan =
+            child instanceof DeviceTableScanNode && !(child instanceof 
AggregationTableScanNode);
+        if (isRawDeviceTableScan
+            && isOrderByTimeOnly(node.getOrderingScheme(), 
(DeviceTableScanNode) child)) {
+          node.setTopKRuntimeFilterSourceId(topKId);
+          ((DeviceTableScanNode) child).setTopKRuntimeFilterSourceId(topKId);
+        } else {
+          child.accept(this, null);
+        }
+      }
+      return node;
+    }
+  }
+
+  private static boolean isOrderByTimeOnly(
+      OrderingScheme orderingScheme, DeviceTableScanNode scanNode) {
+    if (orderingScheme.getOrderBy().size() != 1) {
+      return false;
+    }
+    return scanNode.isTimeColumn(orderingScheme.getOrderBy().get(0));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSource.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSource.java
index 0c86c887fe8..a4184937e2d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSource.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSource.java
@@ -19,11 +19,14 @@
 
 package org.apache.iotdb.db.storageengine.dataregion.read;
 
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
+import org.apache.iotdb.commons.utils.TestOnly;
 import org.apache.iotdb.db.i18n.StorageEngineMessages;
 import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
 
 import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.read.filter.basic.Filter;
+import org.apache.tsfile.utils.BitMap;
 
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -70,6 +73,22 @@ public class QueryDataSource implements IQueryDataSource {
 
   private String databaseName = null;
 
+  /**
+   * Physical seq/unseq TsFile indices pruned by TopK runtime filter at 
resource level. Initialized
+   * only when TopK runtime filter is enabled for this scan.
+   */
+  private BitMap seqInvalidatedByRuntimeFilter;
+
+  private BitMap unseqInvalidatedByRuntimeFilter;
+
+  /**
+   * Remaining seq + unseq TsFileResources that may still contain 
RF-qualifying rows. Initialized
+   * together with the bitmaps above.
+   */
+  private int validSize;
+
+  private boolean runtimeFilterTrackingEnabled;
+
   private static final Comparator<Long> descendingComparator = (o1, o2) -> 
Long.compare(o2, o1);
 
   public QueryDataSource(List<TsFileResource> seqResources, 
List<TsFileResource> unseqResources) {
@@ -91,6 +110,79 @@ public class QueryDataSource implements IQueryDataSource {
     this.unseqResources = other.unseqResources;
     this.unSeqFileOrderIndex = other.unSeqFileOrderIndex;
     this.databaseName = other.databaseName;
+    if (other.runtimeFilterTrackingEnabled) {
+      this.runtimeFilterTrackingEnabled = true;
+      this.validSize = other.validSize;
+      this.seqInvalidatedByRuntimeFilter = 
other.seqInvalidatedByRuntimeFilter.clone();
+      this.unseqInvalidatedByRuntimeFilter = 
other.unseqInvalidatedByRuntimeFilter.clone();
+    }
+  }
+
+  /** Initializes RF tracking state when TopK runtime filter is present for 
this scan. */
+  public void initRuntimeFilterTracking() {
+    if (runtimeFilterTrackingEnabled) {
+      return;
+    }
+    runtimeFilterTrackingEnabled = true;
+    seqInvalidatedByRuntimeFilter = new BitMap(getSeqResourcesSize());
+    unseqInvalidatedByRuntimeFilter = new BitMap(getUnseqResourcesSize());
+    validSize = getSeqResourcesSize() + getUnseqResourcesSize();
+  }
+
+  public boolean isRuntimeFilterTrackingEnabled() {
+    return runtimeFilterTrackingEnabled;
+  }
+
+  @TestOnly
+  public int getValidSize() {
+    return validSize;
+  }
+
+  /** Returns true if any seq/unseq file may still contain RF-qualifying 
resources. */
+  public boolean hasValidResource() {
+    if (!runtimeFilterTrackingEnabled) {
+      return true;
+    }
+    return validSize > 0;
+  }
+
+  /**
+   * Marks the seq file at physical {@code index} as pruned by TopK RF at 
resource level.
+   *
+   * <p>Caller must skip indices for which {@link 
#isRuntimeFilterPruned(boolean, int)} is already
+   * true; this method does not deduplicate marks.
+   */
+  public void setSeqTsFileResourceInvalidated(int physicalIndex) {
+    if (!runtimeFilterTrackingEnabled) {
+      return;
+    }
+    seqInvalidatedByRuntimeFilter.mark(physicalIndex);
+    validSize--;
+  }
+
+  /**
+   * Marks the unseq file at traversal {@code orderIndex} as pruned by TopK RF 
at resource level.
+   *
+   * <p>Caller must skip indices for which {@link 
#isRuntimeFilterPruned(boolean, int)} is already
+   * true; this method does not deduplicate marks.
+   */
+  public void setUnseqTsFileResourceInvalidated(int orderIndex) {
+    if (!runtimeFilterTrackingEnabled) {
+      return;
+    }
+    unseqInvalidatedByRuntimeFilter.mark(unSeqFileOrderIndex[orderIndex]);
+    validSize--;
+  }
+
+  /** Returns true if this file was already pruned by RF at resource level on 
a prior device. */
+  public boolean isRuntimeFilterPruned(boolean isSeq, int index) {
+    if (!runtimeFilterTrackingEnabled) {
+      return false;
+    }
+    if (isSeq) {
+      return seqInvalidatedByRuntimeFilter.isMarked(index);
+    }
+    return 
unseqInvalidatedByRuntimeFilter.isMarked(unSeqFileOrderIndex[index]);
   }
 
   public List<TsFileResource> getSeqResources() {
@@ -144,6 +236,11 @@ public class QueryDataSource implements IQueryDataSource {
     return curSeqSatisfied;
   }
 
+  public boolean isSeqSatisfiedByRuntimeFilter(
+      int curIndex, TopKRuntimeFilter filter, boolean debug) {
+    return isResourceSatisfiedByRuntimeFilter(curIndex, filter, true, debug);
+  }
+
   public long getCurrentSeqOrderTime(int curIndex) {
     if (curIndex != this.curSeqIndex) {
       throw new IllegalArgumentException(
@@ -196,6 +293,10 @@ public class QueryDataSource implements IQueryDataSource {
     return curUnSeqSatisfied;
   }
 
+  public boolean isUnSeqSatisfiedByRuntimeFilter(int curIndex, 
TopKRuntimeFilter filter) {
+    return isResourceSatisfiedByRuntimeFilter(curIndex, filter, false, false);
+  }
+
   public long getCurrentUnSeqOrderTime(int curIndex) {
     if (curIndex != this.curUnSeqIndex) {
       throw new IllegalArgumentException(
@@ -265,6 +366,19 @@ public class QueryDataSource implements IQueryDataSource {
     curUnSeqSatisfied = null;
   }
 
+  private boolean isResourceSatisfiedByRuntimeFilter(
+      int curIndex, TopKRuntimeFilter filter, boolean isSeq, boolean debug) {
+    if (filter == null) {
+      return true;
+    }
+    TsFileResource tsFileResource =
+        isSeq ? seqResources.get(curIndex) : 
unseqResources.get(unSeqFileOrderIndex[curIndex]);
+    // Resource-level RF uses the TsFile's global time range, not per-device 
bounds.
+    long startTime = tsFileResource.getFileStartTime();
+    long endTime = tsFileResource.isClosed() ? tsFileResource.getFileEndTime() 
: Long.MAX_VALUE;
+    return filter.mayQualifyRange(startTime, endTime);
+  }
+
   public String getDatabaseName() {
     if (databaseName == null) {
       List<TsFileResource> resources = !seqResources.isEmpty() ? seqResources 
: unseqResources;
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperatorRuntimeFilterTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperatorRuntimeFilterTest.java
new file mode 100644
index 00000000000..b44e3acfab0
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/ExternalTsFileTableScanOperatorRuntimeFilterTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.source.relational;
+
+import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.db.queryengine.execution.operator.OperatorContext;
+import 
org.apache.iotdb.db.queryengine.execution.operator.source.relational.AbstractTableScanOperator.AbstractTableScanOperatorParameter;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.parameter.SeriesScanOptions;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
+import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.read.common.type.TypeFactory;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ExternalTsFileTableScanOperatorRuntimeFilterTest {
+
+  @Test
+  public void shouldNotStopWhenNonLastDeviceExhausted() throws Exception {
+    ExternalTsFileTableScanOperator operator =
+        new TestExternalTsFileTableScanOperator(createParameter());
+    QueryDataSource firstDeviceDataSource = 
Mockito.mock(QueryDataSource.class);
+    Mockito.when(firstDeviceDataSource.hasValidResource()).thenReturn(false);
+    operator.queryDataSource = firstDeviceDataSource;
+    operator.currentDeviceIndex = 0;
+
+    Assert.assertFalse(operator.shouldStopScanByRuntimeFilter());
+  }
+
+  @Test
+  public void shouldUseCurrentDeviceQueryDataSourceOnLastDevice() throws 
Exception {
+    ExternalTsFileTableScanOperator operator =
+        new TestExternalTsFileTableScanOperator(createParameter());
+    QueryDataSource firstDeviceDataSource = 
Mockito.mock(QueryDataSource.class);
+    Mockito.when(firstDeviceDataSource.hasValidResource()).thenReturn(false);
+    QueryDataSource lastDeviceDataSource = Mockito.mock(QueryDataSource.class);
+    Mockito.when(lastDeviceDataSource.hasValidResource()).thenReturn(true);
+
+    operator.currentDeviceIndex = 1;
+    operator.queryDataSource = lastDeviceDataSource;
+
+    Assert.assertFalse(operator.shouldStopScanByRuntimeFilter());
+
+    Mockito.when(lastDeviceDataSource.hasValidResource()).thenReturn(false);
+    Assert.assertTrue(operator.shouldStopScanByRuntimeFilter());
+  }
+
+  private static AbstractTableScanOperatorParameter createParameter() {
+    OperatorContext operatorContext = Mockito.mock(OperatorContext.class);
+    PlanNodeId sourceId = new PlanNodeId("external-scan");
+    List<ColumnSchema> columnSchemas =
+        Collections.singletonList(
+            new ColumnSchema(
+                "time", TypeFactory.getType(TSDataType.INT64), false, 
TsTableColumnCategory.TIME));
+    int[] columnsIndexArray = new int[] {0};
+    List<DeviceEntry> deviceEntries = Arrays.asList(mockDeviceEntry(), 
mockDeviceEntry());
+    SeriesScanOptions seriesScanOptions =
+        new SeriesScanOptions.Builder().withTopKRuntimeFilter(new 
TopKRuntimeFilter(false)).build();
+    List<String> measurementColumnNames = Collections.emptyList();
+    List<IMeasurementSchema> measurementSchemas = Collections.emptyList();
+    Set<String> allSensors = new HashSet<>();
+    allSensors.add("");
+    return new AbstractTableScanOperatorParameter(
+        allSensors,
+        operatorContext,
+        sourceId,
+        columnSchemas,
+        columnsIndexArray,
+        deviceEntries,
+        Ordering.ASC,
+        seriesScanOptions,
+        measurementColumnNames,
+        measurementSchemas,
+        1000);
+  }
+
+  private static DeviceEntry mockDeviceEntry() {
+    DeviceEntry deviceEntry = Mockito.mock(DeviceEntry.class);
+    IDeviceID deviceID = Mockito.mock(IDeviceID.class);
+    Mockito.when(deviceEntry.getDeviceID()).thenReturn(deviceID);
+    return deviceEntry;
+  }
+
+  private static final class TestExternalTsFileTableScanOperator
+      extends ExternalTsFileTableScanOperator {
+
+    private 
TestExternalTsFileTableScanOperator(AbstractTableScanOperatorParameter 
parameter) {
+      super(parameter, 0);
+    }
+
+    @Override
+    protected void constructAlignedSeriesScanUtil() {
+      // Skip TsFile path construction in unit test.
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
index 01acba6b5d9..6d5102d408a 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
@@ -664,6 +664,31 @@ public final class PlanMatchPattern {
     return node(TopKNode.class, source).with(new TopKMatcher(orderBy, count, 
childrenDataInOrder));
   }
 
+  public static PlanMatchPattern topKWithRuntimeFilterSourceId(
+      String sourceId, PlanMatchPattern... sources) {
+    return topK(sources)
+        .with(
+            TopKNode.class,
+            node -> java.util.Objects.equals(sourceId, 
node.getTopKRuntimeFilterSourceId()));
+  }
+
+  public static PlanMatchPattern topKWithoutRuntimeFilter(PlanMatchPattern... 
sources) {
+    return topK(sources).with(TopKNode.class, node -> 
node.getTopKRuntimeFilterSourceId() == null);
+  }
+
+  public static PlanMatchPattern tableScanWithRuntimeFilter(String 
expectedTableName) {
+    return tableScan(expectedTableName)
+        .with(DeviceTableScanNode.class, node -> 
node.getTopKRuntimeFilterSourceId() != null);
+  }
+
+  public static PlanMatchPattern tableScanWithRuntimeFilter(
+      String expectedTableName, String sourceId) {
+    return tableScan(expectedTableName)
+        .with(
+            DeviceTableScanNode.class,
+            node -> java.util.Objects.equals(sourceId, 
node.getTopKRuntimeFilterSourceId()));
+  }
+
   /*public static PlanMatchPattern topN(long count, List<Ordering> orderBy, 
TopNNode.Step step, PlanMatchPattern source)
   {
       return node(TopNNode.class, source).with(new TopNMatcher(count, orderBy, 
step));
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterDistributedPlanTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterDistributedPlanTest.java
new file mode 100644
index 00000000000..27a1262a539
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterDistributedPlanTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.optimizations;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.UnionNode;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.PlanNodeSearcher;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.anyTree;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.exchange;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScanWithRuntimeFilter;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.topKWithRuntimeFilterSourceId;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.topKWithoutRuntimeFilter;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+
+public class TopKRuntimeFilterDistributedPlanTest {
+
+  private static final String TABLE1 = "testdb.table1";
+  private static final String TABLE2 = "testdb.table2";
+
+  private boolean originalEnableTopKRuntimeFilter;
+
+  @Before
+  public void setUp() {
+    originalEnableTopKRuntimeFilter =
+        IoTDBDescriptor.getInstance().getConfig().isEnableTopKRuntimeFilter();
+    IoTDBDescriptor.getInstance().getConfig().setEnableTopKRuntimeFilter(true);
+    IoTDBDescriptor.getInstance().getConfig().setDataNodeId(1);
+  }
+
+  @After
+  public void tearDown() {
+    IoTDBDescriptor.getInstance()
+        .getConfig()
+        .setEnableTopKRuntimeFilter(originalEnableTopKRuntimeFilter);
+  }
+
+  @Test
+  public void singleRegionKeepsLogicalTopKSourceId() {
+    String sql = "SELECT time FROM table1 WHERE  tag1 = 'shanghai' ORDER BY 
time DESC LIMIT 10";
+    PlanTester planTester = new PlanTester();
+    LogicalQueryPlan logicalPlan = planTester.createPlan(sql);
+
+    TopKNode logicalTopK = findProducerTopK(logicalPlan.getRootNode());
+    String logicalSourceId = logicalTopK.getTopKRuntimeFilterSourceId();
+    Assert.assertEquals(logicalTopK.getPlanNodeId().getId(), logicalSourceId);
+
+    assertPlan(
+        logicalPlan,
+        output(
+            topKWithRuntimeFilterSourceId(
+                logicalSourceId, tableScanWithRuntimeFilter(TABLE1, 
logicalSourceId))));
+
+    PlanNode distributedPlan = planTester.getFragmentPlan(0);
+
+    assertPlan(
+        distributedPlan,
+        output(
+            topKWithRuntimeFilterSourceId(
+                logicalSourceId, tableScanWithRuntimeFilter(TABLE1, 
logicalSourceId))));
+  }
+
+  @Test
+  public void multiRegionCopiesSourceIdToRegionTopKAndClearsRoot() {
+    String sql = "SELECT time FROM table1 ORDER BY time DESC LIMIT 10";
+    PlanTester planTester = new PlanTester();
+    LogicalQueryPlan logicalPlan = planTester.createPlan(sql);
+
+    TopKNode logicalTopK = findProducerTopK(logicalPlan.getRootNode());
+    String logicalSourceId = logicalTopK.getTopKRuntimeFilterSourceId();
+    Assert.assertEquals(logicalTopK.getPlanNodeId().getId(), logicalSourceId);
+
+    assertPlan(
+        logicalPlan,
+        output(
+            topKWithRuntimeFilterSourceId(
+                logicalSourceId, tableScanWithRuntimeFilter(TABLE1, 
logicalSourceId))));
+
+    PlanNode coordinatorFragment = planTester.getFragmentPlan(0);
+    assertPlan(
+        coordinatorFragment,
+        output(anyTree(topKWithoutRuntimeFilter(exchange(), exchange(), 
exchange()))));
+    Assert.assertNull(findProducerTopKOrNull(coordinatorFragment));
+
+    for (int i = 1; i <= 3; i++) {
+      PlanNode regionFragment = planTester.getFragmentPlan(i);
+      assertPlan(
+          regionFragment,
+          topKWithRuntimeFilterSourceId(
+              logicalSourceId, tableScanWithRuntimeFilter(TABLE1, 
logicalSourceId)));
+    }
+  }
+
+  @Test
+  public void unionBranchesKeepDistinctSourceIdsAfterDistribution() {
+    String sql =
+        "(SELECT time FROM table1) UNION ALL (SELECT time FROM table2) ORDER 
BY time DESC LIMIT 10";
+    PlanTester planTester = new PlanTester();
+    LogicalQueryPlan logicalPlan = planTester.createPlan(sql);
+
+    UnionNode unionNode =
+        (UnionNode)
+            PlanNodeSearcher.searchFrom(logicalPlan.getRootNode())
+                .where(UnionNode.class::isInstance)
+                .findFirst()
+                .orElseThrow(AssertionError::new);
+    TopKNode leftBranchTopK = (TopKNode) unionNode.getChildren().get(0);
+    TopKNode rightBranchTopK = (TopKNode) unionNode.getChildren().get(1);
+    String leftSourceId = leftBranchTopK.getTopKRuntimeFilterSourceId();
+    String rightSourceId = rightBranchTopK.getTopKRuntimeFilterSourceId();
+    Assert.assertNotNull(leftSourceId);
+    Assert.assertNotNull(rightSourceId);
+    Assert.assertEquals(leftBranchTopK.getPlanNodeId().getId(), leftSourceId);
+    Assert.assertEquals(rightBranchTopK.getPlanNodeId().getId(), 
rightSourceId);
+    Assert.assertNotEquals(leftSourceId, rightSourceId);
+
+    assertPlan(
+        logicalPlan,
+        output(
+            topKWithoutRuntimeFilter(
+                union(
+                    topKWithRuntimeFilterSourceId(
+                        leftSourceId, tableScanWithRuntimeFilter(TABLE1, 
leftSourceId)),
+                    topKWithRuntimeFilterSourceId(
+                        rightSourceId, tableScanWithRuntimeFilter(TABLE2, 
rightSourceId))))));
+
+    assertPlan(
+        planTester.getFragmentPlan(0),
+        output(topKWithoutRuntimeFilter(union(exchange(), exchange()))));
+
+    assertPlan(
+        planTester.getFragmentPlan(1),
+        topKWithoutRuntimeFilter(exchange(), exchange(), exchange()));
+    for (int i = 2; i <= 4; i++) {
+      assertPlan(
+          planTester.getFragmentPlan(i),
+          topKWithRuntimeFilterSourceId(
+              leftSourceId, tableScanWithRuntimeFilter(TABLE1, leftSourceId)));
+    }
+    assertPlan(
+        planTester.getFragmentPlan(5),
+        topKWithoutRuntimeFilter(exchange(), exchange(), exchange()));
+    for (int i = 6; i <= 8; i++) {
+      assertPlan(
+          planTester.getFragmentPlan(i),
+          topKWithRuntimeFilterSourceId(
+              rightSourceId, tableScanWithRuntimeFilter(TABLE2, 
rightSourceId)));
+    }
+  }
+
+  private static TopKNode findProducerTopK(PlanNode root) {
+    return (TopKNode)
+        PlanNodeSearcher.searchFrom(root)
+            .where(
+                node ->
+                    node instanceof TopKNode
+                        && ((TopKNode) node).getTopKRuntimeFilterSourceId() != 
null)
+            .findFirst()
+            .orElseThrow(AssertionError::new);
+  }
+
+  private static TopKNode findProducerTopKOrNull(PlanNode root) {
+    return (TopKNode)
+        PlanNodeSearcher.searchFrom(root)
+            .where(
+                node ->
+                    node instanceof TopKNode
+                        && ((TopKNode) node).getTopKRuntimeFilterSourceId() != 
null)
+            .findFirst()
+            .orElse(null);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterTreeViewPlanTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterTreeViewPlanTest.java
new file mode 100644
index 00000000000..91b16f17d5e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterTreeViewPlanTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.optimizations;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode;
+import org.apache.iotdb.commons.schema.table.TreeViewSchema;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestMetadata.DEVICE_VIEW_TEST_TABLE;
+import static 
org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestMetadata.TREE_VIEW_DB;
+
+public class TopKRuntimeFilterTreeViewPlanTest {
+
+  private static final String DEFAULT_TREE_DEVICE_VIEW_TABLE_FULL_NAME =
+      String.format("%s.\"%s\"", TREE_VIEW_DB, DEVICE_VIEW_TEST_TABLE);
+
+  @Before
+  public void setup() {
+    TsTable tsTable = new TsTable(DEVICE_VIEW_TEST_TABLE);
+    tsTable.addProp(TsTable.TTL_PROPERTY, Long.MAX_VALUE + "");
+    tsTable.addProp(TreeViewSchema.TREE_PATH_PATTERN, "root.test.**");
+    DataNodeTableCache.getInstance().preUpdateTable(TREE_VIEW_DB, tsTable, 
null);
+    DataNodeTableCache.getInstance().commitUpdateTable(TREE_VIEW_DB, 
DEVICE_VIEW_TEST_TABLE, null);
+  }
+
+  @After
+  public void tearDown() {
+    DataNodeTableCache.getInstance().invalid(TREE_VIEW_DB);
+  }
+
+  @Test
+  public void marksRuntimeFilterOnTreeViewRegionTopKAndScan() {
+    PlanTester planTester = new PlanTester();
+    planTester.createPlan(
+        "SELECT time, s1 FROM "
+            + DEFAULT_TREE_DEVICE_VIEW_TABLE_FULL_NAME
+            + " ORDER BY time DESC LIMIT 10");
+
+    boolean foundProducer = false;
+    boolean foundConsumer = false;
+    String rootTopKId = null;
+    for (int i = 1; i <= 4; i++) {
+      RuntimeFilterMark mark = 
collectRuntimeFilterMark(planTester.getFragmentPlan(i));
+      if (mark.producerTopKId != null) {
+        foundProducer = true;
+        rootTopKId = mark.producerTopKId;
+      }
+      if (mark.scanSourceId != null) {
+        foundConsumer = true;
+        if (rootTopKId != null) {
+          Assert.assertEquals(rootTopKId, mark.scanSourceId);
+        }
+      }
+    }
+
+    Assert.assertTrue(foundProducer);
+    Assert.assertTrue(foundConsumer);
+  }
+
+  private static RuntimeFilterMark collectRuntimeFilterMark(PlanNode root) {
+    RuntimeFilterMark mark = new RuntimeFilterMark();
+    collectRuntimeFilterMark(root, mark);
+    return mark;
+  }
+
+  private static void collectRuntimeFilterMark(PlanNode node, 
RuntimeFilterMark mark) {
+    if (node instanceof TopKNode) {
+      TopKNode topKNode = (TopKNode) node;
+      if (topKNode.getTopKRuntimeFilterSourceId() != null) {
+        mark.producerTopKId = topKNode.getTopKRuntimeFilterSourceId();
+      }
+    }
+    if (node instanceof DeviceTableScanNode) {
+      DeviceTableScanNode scanNode = (DeviceTableScanNode) node;
+      if (scanNode.getTopKRuntimeFilterSourceId() != null) {
+        mark.scanSourceId = scanNode.getTopKRuntimeFilterSourceId();
+        Assert.assertTrue(
+            scanNode instanceof TreeAlignedDeviceViewScanNode
+                || scanNode instanceof TreeNonAlignedDeviceViewScanNode);
+      }
+    }
+    for (PlanNode child : node.getChildren()) {
+      collectRuntimeFilterMark(child, mark);
+    }
+  }
+
+  private static final class RuntimeFilterMark {
+    private String producerTopKId;
+    private String scanSourceId;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSourceRuntimeFilterTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSourceRuntimeFilterTest.java
new file mode 100644
index 00000000000..e6f1b8787f9
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/QueryDataSourceRuntimeFilterTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.storageengine.dataregion.read;
+
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class QueryDataSourceRuntimeFilterTest {
+
+  @Test
+  public void testSeqInvalidatedIndicesAreIndependent() {
+    TsFileResource f0 = mockResource(10, 10);
+    TsFileResource f1 = mockResource(5, 5);
+    TsFileResource f2 = mockResource(20, 20);
+    QueryDataSource dataSource =
+        new QueryDataSource(Arrays.asList(f0, f1, f2), 
Collections.emptyList());
+    dataSource.initRuntimeFilterTracking();
+
+    Assert.assertEquals(3, dataSource.getValidSize());
+
+    dataSource.setSeqTsFileResourceInvalidated(0);
+    dataSource.setSeqTsFileResourceInvalidated(2);
+
+    Assert.assertTrue(dataSource.isRuntimeFilterPruned(true, 0));
+    Assert.assertFalse(dataSource.isRuntimeFilterPruned(true, 1));
+    Assert.assertTrue(dataSource.isRuntimeFilterPruned(true, 2));
+    Assert.assertEquals(1, dataSource.getValidSize());
+    Assert.assertTrue(dataSource.hasValidResource());
+  }
+
+  @Test
+  public void testAllSeqInvalidatedMeansNoValidResource() {
+    TsFileResource f0 = mockResource(1, 1);
+    TsFileResource f1 = mockResource(2, 2);
+    QueryDataSource dataSource =
+        new QueryDataSource(Arrays.asList(f0, f1), Collections.emptyList());
+    dataSource.initRuntimeFilterTracking();
+
+    dataSource.setSeqTsFileResourceInvalidated(0);
+    dataSource.setSeqTsFileResourceInvalidated(1);
+
+    Assert.assertEquals(0, dataSource.getValidSize());
+    Assert.assertFalse(dataSource.hasValidResource());
+  }
+
+  private static TsFileResource mockResource(long startTime, long endTime) {
+    TsFileResource resource = Mockito.mock(TsFileResource.class);
+    Mockito.when(resource.getFileStartTime()).thenReturn(startTime);
+    Mockito.when(resource.isClosed()).thenReturn(true);
+    Mockito.when(resource.getFileEndTime()).thenReturn(endTime);
+    return resource;
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index bfb98ebadc1..0150f9db6fa 100644
--- 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -1438,6 +1438,14 @@ recovery_log_interval_in_ms=5000
 # Datatype: boolean
 enable_tsfile_validation=false
 
+# Whether to enable the TopK runtime filter optimization for table-model
+# "ORDER BY time LIMIT k" queries. When enabled, the TopK operator feeds the 
current
+# heap-top time back to table scans so scans can skip files/rows that cannot 
enter the
+# final result. Set to false to fall back to the original execution path 
without restart.
+# effectiveMode: hot_reload
+# Datatype: boolean
+enable_topk_runtime_filter=true
+
 # Default tier TTL. When the survival time of the data exceeds the threshold, 
it will be migrated to the next tier.
 # Negative value means the tier TTL is unlimited.
 # effectiveMode: restart
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/TopKNode.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/TopKNode.java
index 34376dcdbd5..a28e39a74cc 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/TopKNode.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/TopKNode.java
@@ -32,6 +32,8 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
 import com.google.common.base.Objects;
 import org.apache.tsfile.utils.ReadWriteIOUtils;
 
+import javax.annotation.Nullable;
+
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -50,6 +52,14 @@ public class TopKNode extends MultiChildProcessNode {
 
   private final boolean childrenDataInOrder;
 
+  private final transient boolean topKRuntimeFilterAscending;
+
+  // Root TopK plan node id under which the shared runtime filter is 
registered/looked up; a
+  // non-null value also marks this TopK as a runtime filter producer (set 
during distributed
+  // optimize). All per-region TopK producers and their scan consumers within 
the same query use
+  // this id so that fragments of the same query on one DataNode share a 
single filter instance.
+  @Nullable private String topKRuntimeFilterSourceId;
+
   public TopKNode(
       PlanNodeId id,
       OrderingScheme scheme,
@@ -61,6 +71,7 @@ public class TopKNode extends MultiChildProcessNode {
     this.count = count;
     this.outputSymbols = outputSymbols;
     this.childrenDataInOrder = childrenDataInOrder;
+    this.topKRuntimeFilterAscending = 
computeTopKRuntimeFilterAscending(scheme);
   }
 
   public TopKNode(
@@ -75,11 +86,20 @@ public class TopKNode extends MultiChildProcessNode {
     this.count = count;
     this.outputSymbols = outputSymbols;
     this.childrenDataInOrder = childrenDataInOrder;
+    this.topKRuntimeFilterAscending = 
computeTopKRuntimeFilterAscending(scheme);
+  }
+
+  private static boolean computeTopKRuntimeFilterAscending(OrderingScheme 
orderingScheme) {
+    Symbol orderBy = orderingScheme.getOrderBy().get(0);
+    return orderingScheme.getOrdering(orderBy).isAscending();
   }
 
   @Override
   public PlanNode clone() {
-    return new TopKNode(getPlanNodeId(), orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    TopKNode cloned =
+        new TopKNode(getPlanNodeId(), orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    cloned.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+    return cloned;
   }
 
   @Override
@@ -102,6 +122,7 @@ public class TopKNode extends MultiChildProcessNode {
       Symbol.serialize(symbol, byteBuffer);
     }
     ReadWriteIOUtils.write(childrenDataInOrder, byteBuffer);
+    ReadWriteIOUtils.write(topKRuntimeFilterSourceId, byteBuffer);
   }
 
   @Override
@@ -114,6 +135,7 @@ public class TopKNode extends MultiChildProcessNode {
       Symbol.serialize(symbol, stream);
     }
     ReadWriteIOUtils.write(childrenDataInOrder, stream);
+    ReadWriteIOUtils.write(topKRuntimeFilterSourceId, stream);
   }
 
   public static TopKNode deserialize(ByteBuffer byteBuffer) {
@@ -125,8 +147,12 @@ public class TopKNode extends MultiChildProcessNode {
       outputSymbols.add(Symbol.deserialize(byteBuffer));
     }
     boolean childrenDataInOrder = ReadWriteIOUtils.readBool(byteBuffer);
+    String topKRuntimeFilterSourceId = ReadWriteIOUtils.readString(byteBuffer);
     PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer);
-    return new TopKNode(planNodeId, orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    TopKNode topKNode =
+        new TopKNode(planNodeId, orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    topKNode.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+    return topKNode;
   }
 
   @Override
@@ -139,7 +165,10 @@ public class TopKNode extends MultiChildProcessNode {
     checkArgument(
         children.size() == newChildren.size(),
         QueryMessages.EXCEPTION_WRONG_NUMBER_OF_NEW_CHILDREN_817AF800);
-    return new TopKNode(id, newChildren, orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    TopKNode topKNode =
+        new TopKNode(id, newChildren, orderingScheme, count, outputSymbols, 
childrenDataInOrder);
+    topKNode.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+    return topKNode;
   }
 
   public OrderingScheme getOrderingScheme() {
@@ -154,6 +183,20 @@ public class TopKNode extends MultiChildProcessNode {
     return childrenDataInOrder;
   }
 
+  public boolean isTopKRuntimeFilterAscending() {
+    return topKRuntimeFilterAscending;
+  }
+
+  /** A non-null source id marks this TopK as a runtime filter producer. */
+  @Nullable
+  public String getTopKRuntimeFilterSourceId() {
+    return topKRuntimeFilterSourceId;
+  }
+
+  public void setTopKRuntimeFilterSourceId(@Nullable String 
topKRuntimeFilterSourceId) {
+    this.topKRuntimeFilterSourceId = topKRuntimeFilterSourceId;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
@@ -162,12 +205,14 @@ public class TopKNode extends MultiChildProcessNode {
     TopKNode sortNode = (TopKNode) o;
     return Objects.equal(orderingScheme, sortNode.orderingScheme)
         && Objects.equal(outputSymbols, sortNode.outputSymbols)
-        && Objects.equal(count, sortNode.count);
+        && Objects.equal(count, sortNode.count)
+        && Objects.equal(topKRuntimeFilterSourceId, 
sortNode.topKRuntimeFilterSourceId);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(super.hashCode(), orderingScheme, outputSymbols, 
count);
+    return Objects.hashCode(
+        super.hashCode(), orderingScheme, outputSymbols, count, 
topKRuntimeFilterSourceId);
   }
 
   @Override

Reply via email to