This is an automated email from the ASF dual-hosted git repository. Wei-hao-Li pushed a commit to branch lwh/topK in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 0bb8159720a604bc947343c46647a47c17dc4f6b Author: Weihao Li <[email protected]> AuthorDate: Fri Jul 10 10:51:46 2026 +0800 draft Signed-off-by: Weihao Li <[email protected]> --- .../calc/execution/filter/TopKRuntimeFilter.java | 67 ++++++++ .../operator/process/TableTopKOperator.java | 28 ++- .../execution/operator/process/TopKOperator.java | 33 ++++ .../calc/plan/planner/TableOperatorGenerator.java | 12 +- .../execution/filter/TopKRuntimeFilterTest.java | 73 ++++++++ .../execution/fragment/DataNodeQueryContext.java | 21 +++ .../execution/operator/source/SeriesScanUtil.java | 129 +++++++++++++- .../relational/AbstractTableScanOperator.java | 43 +++-- .../planner/DataNodeTableOperatorGenerator.java | 189 ++++++++++++++++++++- .../plan/planner/LocalExecutionPlanner.java | 2 + .../plan/planner/TopKRuntimeFilterBinder.java | 60 +++++++ .../plan/planner/plan/node/PlanGraphPrinter.java | 6 + .../planner/plan/parameter/SeriesScanOptions.java | 19 ++- .../distribute/TableDistributedPlanner.java | 23 ++- .../planner/node/DeviceTableScanNode.java | 60 +++++-- .../optimizations/TopKRuntimeFilterOptimizer.java | 95 +++++++++++ .../optimizations/TopKRuntimeFilterUtils.java | 74 ++++++++ .../dataregion/read/QueryDataSource.java | 115 +++++++++++++ .../TopKRuntimeFilterOptimizerTest.java | 78 +++++++++ .../plan/relational/planner/node/TopKNode.java | 59 ++++++- 20 files changed, 1143 insertions(+), 43 deletions(-) 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..aaf270369fe 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,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.plan.planner.CommonOperatorUtils; @@ -33,6 +34,24 @@ import java.util.Comparator; import java.util.List; public class TableTopKOperator extends TopKOperator { + public TableTopKOperator( + CommonOperatorContext operatorContext, + List<Operator> childrenOperators, + List<TSDataType> dataTypes, + Comparator<SortKey> comparator, + int topValue, + boolean childrenDataInOrder, + TopKRuntimeFilter topKRuntimeFilter) { + super( + operatorContext, + childrenOperators, + dataTypes, + comparator, + topValue, + childrenDataInOrder, + topKRuntimeFilter); + } + public TableTopKOperator( CommonOperatorContext operatorContext, List<Operator> childrenOperators, @@ -40,7 +59,14 @@ public class TableTopKOperator extends TopKOperator { Comparator<SortKey> comparator, int topValue, boolean childrenDataInOrder) { - super(operatorContext, childrenOperators, dataTypes, comparator, topValue, childrenDataInOrder); + this( + operatorContext, + childrenOperators, + dataTypes, + comparator, + topValue, + childrenDataInOrder, + null); } @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..54d58e85bdd 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) { @@ -425,4 +448,14 @@ public abstract class TopKOperator implements ProcessOperator { getOperator(i).close(); childrenOperators.set(i, null); } + + private void updateTopKRuntimeFilter() { + if (topKRuntimeFilter == null || mergeSortHeap.getHeapSize() < topValue) { + return; + } + MergeSortKey peek = mergeSortHeap.peek(); + if (peek != null) { + topKRuntimeFilter.updateThreshold(peek.tsBlock.getTimeByIndex(peek.rowIndex)); + } + } } 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..e44379c9c5d 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; @@ -910,7 +911,16 @@ public abstract class TableOperatorGenerator< getComparatorForTable( node.getOrderingScheme().getOrderingList(), sortItemIndexList, sortItemDataTypeList), (int) node.getCount(), - node.isChildrenDataInOrder()); + node.isChildrenDataInOrder(), + getTopKRuntimeFilter(context, node)); + } + + protected TopKRuntimeFilter getTopKRuntimeFilter(C context, TopKNode node) { + return null; + } + + protected TopKRuntimeFilter getTopKRuntimeFilter(C context) { + return getTopKRuntimeFilter(context, null); } protected List<TSDataType> getOutputColumnTypes(PlanNode node, ITableTypeProvider typeProvider) { 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/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..ad87d994983 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(); @@ -276,7 +278,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 +341,25 @@ 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 void skipByTopKRuntimeFilter( + Statistics<? extends Serializable> statistics, Runnable skip) { + if (!mayQualifyRuntimeFilterRange(statistics)) { + skip.run(); + } + } + @SuppressWarnings("squid:S3740") public Statistics currentFileTimeStatistics() { return firstTimeSeriesMetadata.getTimeStatistics(); @@ -369,7 +390,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 +440,11 @@ public class SeriesScanUtil implements Accountable { return; } + skipByTopKRuntimeFilter(firstChunkMetadata.getStatistics(), this::skipCurrentChunk); + if (firstChunkMetadata == null) { + return; + } + // globalTimeFilter.canSkip() must be FALSE Filter pushDownFilter = scanOptions.getPushDownFilter(); if (pushDownFilter != null && pushDownFilter.canSkip(firstChunkMetadata)) { @@ -542,7 +568,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 +945,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 +967,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 +1461,11 @@ public class SeriesScanUtil implements Accountable { return; } + skipByTopKRuntimeFilter(firstPageReader.getStatistics(), this::skipCurrentPage); + if (firstPageReader == null) { + return; + } + IPageReader pageReader = firstPageReader.getPageReader(); // globalTimeFilter.canSkip() must be FALSE @@ -1892,6 +1952,11 @@ public class SeriesScanUtil implements Accountable { return; } + skipByTopKRuntimeFilter(firstTimeSeriesMetadata.getStatistics(), this::skipCurrentFile); + if (firstTimeSeriesMetadata == null) { + return; + } + // globalTimeFilter.canSkip() must be FALSE Filter pushDownFilter = scanOptions.getPushDownFilter(); if (pushDownFilter != null && pushDownFilter.canSkip(firstTimeSeriesMetadata)) { @@ -2393,9 +2458,24 @@ 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, false)) { + curSeqFileIndex--; + continue; + } if (dataSource.isSeqSatisfied( deviceID, curSeqFileIndex, scanOptions.getGlobalTimeFilter(), false)) { + if (filter == null + || dataSource.isSeqSatisfiedByRuntimeFilter( + deviceID, curSeqFileIndex, filter, false)) { + break; + } + dataSource.truncateSeqValidSizeForRuntimeFilter(curSeqFileIndex, false); + if (!dataSource.hasValidResource()) { + runtimeFilterExhausted = true; + } + curSeqFileIndex = -1; break; } curSeqFileIndex--; @@ -2405,10 +2485,22 @@ 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, false)) { + curUnseqFileIndex++; + continue; + } if (dataSource.isUnSeqSatisfied( deviceID, curUnseqFileIndex, scanOptions.getGlobalTimeFilter(), false)) { - break; + if (filter == null + || dataSource.isUnSeqSatisfiedByRuntimeFilter(curUnseqFileIndex, filter, false)) { + break; + } + dataSource.decreaseValidSizeForRuntimeFilter(false, curUnseqFileIndex, false); + if (!dataSource.hasValidResource()) { + runtimeFilterExhausted = true; + } } curUnseqFileIndex++; } @@ -2522,9 +2614,24 @@ 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, true)) { + curSeqFileIndex++; + continue; + } if (dataSource.isSeqSatisfied( deviceID, curSeqFileIndex, scanOptions.getGlobalTimeFilter(), false)) { + if (filter == null + || dataSource.isSeqSatisfiedByRuntimeFilter( + deviceID, curSeqFileIndex, filter, false)) { + break; + } + dataSource.truncateSeqValidSizeForRuntimeFilter(curSeqFileIndex, true); + if (!dataSource.hasValidResource()) { + runtimeFilterExhausted = true; + } + curSeqFileIndex = dataSource.getSeqResourcesSize(); break; } curSeqFileIndex++; @@ -2534,10 +2641,22 @@ 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, true)) { + curUnseqFileIndex++; + continue; + } if (dataSource.isUnSeqSatisfied( deviceID, curUnseqFileIndex, scanOptions.getGlobalTimeFilter(), false)) { - break; + if (filter == null + || dataSource.isUnSeqSatisfiedByRuntimeFilter(curUnseqFileIndex, filter, false)) { + break; + } + dataSource.decreaseValidSizeForRuntimeFilter(false, curUnseqFileIndex, true); + if (!dataSource.hasValidResource()) { + runtimeFilterExhausted = true; + } } 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..383be946624 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 @@ -217,8 +217,17 @@ 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; + } + // QueryDataSource 中 seq/unseq 均无 RF 候选文件时,立即结束(hasNext → false) + return shouldStopScanByRuntimeFilter(); } @Override @@ -241,8 +250,8 @@ public abstract class AbstractTableScanOperator extends AbstractSeriesScanOperat @Override public void initQueryDataSource(IQueryDataSource dataSource) { this.queryDataSource = (QueryDataSource) dataSource; - if (this.seriesScanUtil != null) { - this.seriesScanUtil.initQueryDataSource(queryDataSource); + if (currentDeviceIndex < deviceCount) { + setupCurrentDeviceScan(); } this.resultTsBlockBuilder = new TsBlockBuilder(getResultDataTypes()); this.resultTsBlockBuilder.setMaxTsBlockLineNumber(this.maxTsBlockLineNum); @@ -251,19 +260,29 @@ public abstract class AbstractTableScanOperator extends AbstractSeriesScanOperat } protected void moveToNextDevice() { + if (shouldStopScanByRuntimeFilter()) { + currentDeviceIndex = deviceCount; + return; + } currentDeviceIndex++; if (currentDeviceIndex < deviceCount) { - // construct AlignedSeriesScanUtil for next device - constructAlignedSeriesScanUtil(); - - // reset QueryDataSource - queryDataSource.reset(); - this.seriesScanUtil.initQueryDataSource(queryDataSource); - this.operatorContext.recordSpecifiedInfo( - CommonOperatorUtils.CURRENT_DEVICE_INDEX_STRING, Integer.toString(currentDeviceIndex)); + setupCurrentDeviceScan(); } } + /** Returns true when file-level RF has pruned all seq/unseq files — scan can stop globally. */ + private boolean shouldStopScanByRuntimeFilter() { + return seriesScanOptions.getTopKRuntimeFilter() != null && !queryDataSource.hasValidResource(); + } + + private void setupCurrentDeviceScan() { + constructAlignedSeriesScanUtil(); + queryDataSource.reset(); + this.seriesScanUtil.initQueryDataSource(queryDataSource); + this.operatorContext.recordSpecifiedInfo( + CommonOperatorUtils.CURRENT_DEVICE_INDEX_STRING, Integer.toString(currentDeviceIndex)); + } + 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/plan/planner/DataNodeTableOperatorGenerator.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/DataNodeTableOperatorGenerator.java index a17726d9dc2..e482bf5f9c0 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,11 +19,13 @@ 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; import org.apache.iotdb.calc.execution.operator.process.LimitOperator; import org.apache.iotdb.calc.execution.operator.process.OffsetOperator; +import org.apache.iotdb.calc.execution.operator.process.TableTopKOperator; import org.apache.iotdb.calc.execution.operator.source.relational.aggregation.LastDescAccumulator; import org.apache.iotdb.calc.execution.operator.source.relational.aggregation.TableAggregator; import org.apache.iotdb.calc.execution.relational.ColumnTransformerBuilder; @@ -46,6 +48,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; @@ -185,6 +188,7 @@ import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; +import static org.apache.iotdb.calc.execution.operator.process.join.merge.MergeSortComparator.getComparatorForTable; import static org.apache.iotdb.calc.plan.relational.planner.ir.GlobalTimePredicateExtractVisitor.isTimeColumn; import static org.apache.iotdb.calc.utils.constant.SqlConstant.AVG; import static org.apache.iotdb.calc.utils.constant.SqlConstant.COUNT; @@ -222,6 +226,86 @@ 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 == null || !node.isUseTopKRuntimeFilter() || context.dataNodeQueryContext == null) { + return null; + } + return context.dataNodeQueryContext.registerTopKRuntimeFilter( + node.getPlanNodeId().getId(), 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 == null + || context.dataNodeQueryContext == null + || !scanNode.getTopKRuntimeFilterSourceId().isPresent()) { + return null; + } + return context.dataNodeQueryContext.getTopKRuntimeFilter( + scanNode.getTopKRuntimeFilterSourceId().get().getId()); + } + + @Override + protected TopKRuntimeFilter getTopKRuntimeFilter( + 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 + public Operator visitTopK(TopKNode node, LocalExecutionPlanContext context) { + TopKRuntimeFilter filter = registerTopKRuntimeFilterForTopK(node, context); + + CommonOperatorContext operatorContext = + addOperatorContext(context, node.getPlanNodeId(), TableTopKOperator.class.getSimpleName()); + List<Operator> children = new ArrayList<>(node.getChildren().size()); + for (PlanNode child : node.getChildren()) { + children.add(this.process(child, context)); + } + List<TSDataType> dataTypes = getOutputColumnTypes(node, context.getTableTypeProvider()); + int sortItemsCount = node.getOrderingScheme().getOrderBy().size(); + + List<Integer> sortItemIndexList = new ArrayList<>(sortItemsCount); + List<TSDataType> sortItemDataTypeList = new ArrayList<>(sortItemsCount); + genSortInformation( + node.getOutputSymbols(), + node.getOrderingScheme(), + sortItemIndexList, + sortItemDataTypeList, + context.getTableTypeProvider()); + return new TableTopKOperator( + operatorContext, + children, + dataTypes, + getComparatorForTable( + node.getOrderingScheme().getOrderingList(), sortItemIndexList, sortItemDataTypeList), + (int) node.getCount(), + node.isChildrenDataInOrder(), + filter); + } + @Override protected String getSortTmpDir(CommonOperatorContext operatorContext) { OperatorContext dataNodeOperatorContext = (OperatorContext) operatorContext; @@ -626,6 +710,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); @@ -1091,16 +1176,84 @@ 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) { + return constructAbstractTableScanOperatorParameter(node, context, null); + } + + private AbstractTableScanOperator.AbstractTableScanOperatorParameter + constructAbstractTableScanOperatorParameter( + 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 +1272,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); @@ -1932,6 +2086,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 +2132,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/LocalExecutionPlanner.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java index a093c7832fd..0cb2b157051 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java @@ -112,6 +112,8 @@ public class LocalExecutionPlanner { LocalExecutionPlanContext context = new LocalExecutionPlanContext(types, instanceContext, dataNodeQueryContext); + TopKRuntimeFilterBinder.bind(plan, dataNodeQueryContext); + Operator root = generateOperator(instanceContext, context, plan); PipelineMemoryEstimator memoryEstimator = diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TopKRuntimeFilterBinder.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TopKRuntimeFilterBinder.java new file mode 100644 index 00000000000..70b2e62abe5 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TopKRuntimeFilterBinder.java @@ -0,0 +1,60 @@ +/* + * 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.planner; + +import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter; +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.db.queryengine.execution.fragment.DataNodeQueryContext; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor; + +/** + * Binds {@link TopKRuntimeFilter} runtime objects according to plan marks set by {@link + * org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.TopKRuntimeFilterOptimizer}. + */ +public class TopKRuntimeFilterBinder { + + private TopKRuntimeFilterBinder() {} + + public static void bind(PlanNode planRoot, DataNodeQueryContext dataNodeQueryContext) { + planRoot.accept(new Binder(), dataNodeQueryContext); + } + + private static class Binder implements PlanVisitor<Void, DataNodeQueryContext> { + + @Override + public Void visitPlan(PlanNode node, DataNodeQueryContext context) { + for (PlanNode child : node.getChildren()) { + child.accept(this, context); + } + return null; + } + + @Override + public Void visitTopK(TopKNode node, DataNodeQueryContext context) { + if (node.isUseTopKRuntimeFilter()) { + context.registerTopKRuntimeFilter( + node.getPlanNodeId().getId(), + new TopKRuntimeFilter(node.isTopKRuntimeFilterAscending())); + } + return visitPlan(node, context); + } + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java index 06aba49200b..5dd36542656 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,9 @@ public class PlanGraphPrinter implements PlanVisitor<List<String>, PlanGraphPrin boxValue.add( String.format( "PushDownLimitToEachDevice: %s", deviceTableScanNode.isPushLimitToEachDevice())); + deviceTableScanNode + .getTopKRuntimeFilterSourceId() + .ifPresent(sourceId -> boxValue.add(String.format("TOPN OPT: %s", sourceId.getId()))); } boxValue.add( @@ -1049,6 +1052,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.isUseTopKRuntimeFilter()) { + 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/TableDistributedPlanner.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanner.java index fd9bc35fb4c..893420125d7 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 @@ -47,6 +47,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode import org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.DataNodeLocationSupplierFactory; import org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.DistributedOptimizeFactory; import org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.PlanOptimizer; +import org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations.TopKRuntimeFilterOptimizer; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ExplainOutputFormat; import java.util.Collections; @@ -176,7 +177,27 @@ 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); + + // Mark TopK runtime filter producer/consumer after exchange insertion. + if (analysis.isQuery()) { + planWithExchange = + new TopKRuntimeFilterOptimizer() + .optimize( + planWithExchange, + new PlanOptimizer.Context( + mppQueryContext.getSession(), + analysis, + metadata, + mppQueryContext, + new SymbolAllocator(), + mppQueryContext.getQueryId(), + NOOP, + PlanOptimizersStatsCollector.createPlanOptimizersStatsCollector())); + } + + 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..bc7773a50c8 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 PlanNodeId 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,13 @@ public class DeviceTableScanNode extends TableScanNode { } ReadWriteIOUtils.write(node.pushLimitToEachDevice, byteBuffer); + + if (node.topKRuntimeFilterSourceId != null) { + ReadWriteIOUtils.write(true, byteBuffer); + node.topKRuntimeFilterSourceId.serialize(byteBuffer); + } else { + ReadWriteIOUtils.write(false, byteBuffer); + } } protected static void serializeMemberVariables( @@ -198,6 +211,13 @@ public class DeviceTableScanNode extends TableScanNode { } ReadWriteIOUtils.write(node.pushLimitToEachDevice, stream); + + if (node.topKRuntimeFilterSourceId != null) { + ReadWriteIOUtils.write(true, stream); + node.topKRuntimeFilterSourceId.serialize(stream); + } else { + ReadWriteIOUtils.write(false, stream); + } } protected static void deserializeMemberVariables( @@ -227,6 +247,10 @@ public class DeviceTableScanNode extends TableScanNode { } node.pushLimitToEachDevice = ReadWriteIOUtils.readBool(byteBuffer); + + if (ReadWriteIOUtils.readBool(byteBuffer)) { + node.topKRuntimeFilterSourceId = PlanNodeId.deserialize(byteBuffer); + } } @Override @@ -307,6 +331,14 @@ public class DeviceTableScanNode extends TableScanNode { this.containsNonAlignedDevice = true; } + public Optional<PlanNodeId> getTopKRuntimeFilterSourceId() { + return Optional.ofNullable(topKRuntimeFilterSourceId); + } + + public void setTopKRuntimeFilterSourceId(PlanNodeId 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/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..f56000b9f89 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizer.java @@ -0,0 +1,95 @@ +/* + * 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.planner.plan.node.PlanNodeId; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode; + +/** + * <b>Optimization phase:</b> Distributed plan planning (after exchange nodes are inserted). + * + * <p>Marks TopK and table scan nodes for TopK runtime filter, similar to Doris {@code TOPN OPT} / + * {@code TOPN OPT: N} plan annotations. + */ +public class TopKRuntimeFilterOptimizer implements PlanOptimizer { + + @Override + public PlanNode optimize(PlanNode plan, Context context) { + if (!context.getAnalysis().isQuery()) { + return plan; + } + return plan.accept(new Rewriter(), null); + } + + private static class Rewriter implements PlanVisitor<PlanNode, Void> { + + @Override + public PlanNode visitPlan(PlanNode node, Void context) { + PlanNode newNode = node.clone(); + for (PlanNode child : node.getChildren()) { + newNode.addChild(child.accept(this, context)); + } + return newNode; + } + + @Override + public PlanNode visitTopK(TopKNode node, Void context) { + TopKNode topKNode = (TopKNode) node.clone(); + for (PlanNode child : node.getChildren()) { + topKNode.addChild(child.accept(this, context)); + } + + if (TopKRuntimeFilterUtils.isOrderByTimeOnly(topKNode.getOrderingScheme()) + && TopKRuntimeFilterUtils.qualifiesForRuntimeFilter(topKNode)) { + topKNode.setUseTopKRuntimeFilter(true); + topKNode.setTopKRuntimeFilterAscending( + topKNode + .getOrderingScheme() + .getOrdering(topKNode.getOrderingScheme().getOrderBy().get(0)) + .isAscending()); + markScansInSubtree(topKNode, topKNode.getPlanNodeId()); + } + return topKNode; + } + + private void markScansInSubtree(PlanNode root, PlanNodeId sourceId) { + root.accept( + new PlanVisitor<Void, Void>() { + @Override + public Void visitPlan(PlanNode node, Void unused) { + for (PlanNode child : node.getChildren()) { + child.accept(this, null); + } + return null; + } + + @Override + public Void visitDeviceTableScan(DeviceTableScanNode scanNode, Void unused) { + scanNode.setTopKRuntimeFilterSourceId(sourceId); + return null; + } + }, + null); + } + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterUtils.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterUtils.java new file mode 100644 index 00000000000..a0704a72361 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterUtils.java @@ -0,0 +1,74 @@ +/* + * 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.Symbol; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class TopKRuntimeFilterUtils { + + private TopKRuntimeFilterUtils() {} + + public static boolean isOrderByTimeOnly(OrderingScheme orderingScheme) { + if (orderingScheme.getOrderBy().size() != 1) { + return false; + } + Symbol orderBy = orderingScheme.getOrderBy().get(0); + return "time".equalsIgnoreCase(orderBy.getName()); + } + + public static boolean qualifiesForRuntimeFilter(TopKNode topKNode) { + AtomicBoolean hasTableScan = new AtomicBoolean(false); + AtomicBoolean hasExchange = new AtomicBoolean(false); + PlanVisitor<Void, Void> subtreeDetector = + new PlanVisitor<Void, Void>() { + @Override + public Void visitPlan(PlanNode node, Void unused) { + for (PlanNode child : node.getChildren()) { + child.accept(this, null); + } + return null; + } + + @Override + public Void visitDeviceTableScan(DeviceTableScanNode scanNode, Void unused) { + hasTableScan.set(true); + return null; + } + + @Override + public Void visitTableExchange(ExchangeNode exchangeNode, Void unused) { + hasExchange.set(true); + return null; + } + }; + for (PlanNode child : topKNode.getChildren()) { + child.accept(subtreeDetector, null); + } + return hasTableScan.get() && !hasExchange.get(); + } +} 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..3d5b724754b 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,6 +19,7 @@ package org.apache.iotdb.db.storageengine.dataregion.read; +import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter; import org.apache.iotdb.db.i18n.StorageEngineMessages; import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource; @@ -70,11 +71,21 @@ public class QueryDataSource implements IQueryDataSource { private String databaseName = null; + /** + * Remaining seq/unseq TsFileResources that may still contain rows qualifying for TopK runtime + * filter. Initialized to list sizes and decremented when a file is skipped at resource level. + * When both counters reach zero, the scan can exit early. + */ + private int seqValidSize; + + private int unseqValidSize; + private static final Comparator<Long> descendingComparator = (o1, o2) -> Long.compare(o2, o1); public QueryDataSource(List<TsFileResource> seqResources, List<TsFileResource> unseqResources) { this.seqResources = seqResources; this.unseqResources = unseqResources; + initValidSize(); } public QueryDataSource( @@ -82,6 +93,7 @@ public class QueryDataSource implements IQueryDataSource { this.seqResources = seqResources; this.unseqResources = unseqResources; this.databaseName = databaseName; + initValidSize(); } // used for compaction, because in compaction task(unlike query, each QueryDataSource only serve @@ -91,6 +103,83 @@ public class QueryDataSource implements IQueryDataSource { this.unseqResources = other.unseqResources; this.unSeqFileOrderIndex = other.unSeqFileOrderIndex; this.databaseName = other.databaseName; + this.seqValidSize = other.seqValidSize; + this.unseqValidSize = other.unseqValidSize; + } + + /** Initializes seq/unseq valid resource counters from list sizes. */ + private void initValidSize() { + seqValidSize = getSeqResourcesSize(); + unseqValidSize = getUnseqResourcesSize(); + } + + public int getSeqValidSize() { + return seqValidSize; + } + + public int getUnseqValidSize() { + return unseqValidSize; + } + + /** Returns true if either seq or unseq may still contain RF-qualifying resources. */ + public boolean hasValidResource() { + return seqValidSize > 0 || unseqValidSize > 0; + } + + /** + * Decrements the remaining file count when a TsFileResource is pruned by TopK runtime filter at + * resource level. Counters are shared across all devices in this scan; when both reach zero, the + * entire scan can stop without switching to subsequent devices. + */ + public void decreaseValidSize(boolean isSeq) { + if (isSeq) { + if (seqValidSize > 0) { + seqValidSize--; + } + } else if (unseqValidSize > 0) { + unseqValidSize--; + } + } + + /** + * Decrements validSize when a TsFileResource is first pruned by TopK runtime filter at resource + * level. File lists are time-ordered, so RF pruning is monotonic from one end: ASC prunes a + * suffix, DESC prunes a prefix. {@link #seqValidSize} / {@link #unseqValidSize} then double as + * the cross-device scan watermark. + */ + public void decreaseValidSizeForRuntimeFilter(boolean isSeq, int index, boolean ascending) { + if (isRuntimeFilterPruned(isSeq, index, ascending)) { + return; + } + decreaseValidSize(isSeq); + } + + /** + * Truncates seq validSize when RF fails at {@code index}. Seq files are time-ordered, so all + * files on the pruned side of {@code index} can be excluded at once: ASC suffix {@code [index, + * size)}, DESC prefix {@code [0, index]}. + */ + public void truncateSeqValidSizeForRuntimeFilter(int index, boolean ascending) { + int size = getSeqResourcesSize(); + if (ascending) { + seqValidSize = Math.min(seqValidSize, index); + } else { + seqValidSize = Math.min(seqValidSize, size - index - 1); + } + } + + /** + * Returns true if this file index was already pruned by RF at resource level on a prior device. + */ + public boolean isRuntimeFilterPruned(boolean isSeq, int index, boolean ascending) { + int validSize = isSeq ? seqValidSize : unseqValidSize; + int size = isSeq ? getSeqResourcesSize() : getUnseqResourcesSize(); + if (ascending) { + // ASC: pruned files form suffix [validSize, size) + return index >= validSize; + } + // DESC: pruned files form prefix [0, size - validSize) + return index < size - validSize; } public List<TsFileResource> getSeqResources() { @@ -144,6 +233,11 @@ public class QueryDataSource implements IQueryDataSource { return curSeqSatisfied; } + public boolean isSeqSatisfiedByRuntimeFilter( + IDeviceID deviceID, 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 +290,11 @@ public class QueryDataSource implements IQueryDataSource { return curUnSeqSatisfied; } + public boolean isUnSeqSatisfiedByRuntimeFilter( + int curIndex, TopKRuntimeFilter filter, boolean debug) { + return isResourceSatisfiedByRuntimeFilter(curIndex, filter, false, debug); + } + public long getCurrentUnSeqOrderTime(int curIndex) { if (curIndex != this.curUnSeqIndex) { throw new IllegalArgumentException( @@ -265,6 +364,22 @@ 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]); + if (tsFileResource == null) { + return false; + } + // 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/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizerTest.java new file mode 100644 index 00000000000..c67506f3c0b --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TopKRuntimeFilterOptimizerTest.java @@ -0,0 +1,78 @@ +/* + * 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.PlanNodeId; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.OrderingScheme; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNode; +import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.Optional; + +public class TopKRuntimeFilterOptimizerTest { + + @Test + public void marksTopKAndScanWhenQualified() { + PlanNodeId topKId = new PlanNodeId("topk"); + PlanNodeId scanId = new PlanNodeId("scan"); + TopKNode topKNode = createTopK(topKId, SortOrder.ASC_NULLS_LAST); + DeviceTableScanNode scanNode = createScan(scanId); + topKNode.addChild(new LimitNode(new PlanNodeId("limit"), scanNode, 10, Optional.empty())); + + TopKNode optimized = (TopKNode) new TopKRuntimeFilterOptimizer().optimize(topKNode, null); + + Assert.assertTrue(optimized.isUseTopKRuntimeFilter()); + Assert.assertTrue(optimized.isTopKRuntimeFilterAscending()); + DeviceTableScanNode optimizedScan = + (DeviceTableScanNode) optimized.getChildren().get(0).getChildren().get(0); + Assert.assertEquals(topKId, optimizedScan.getTopKRuntimeFilterSourceId().orElse(null)); + } + + @Test + public void skipsTopKWithExchangeChild() { + PlanNodeId topKId = new PlanNodeId("topk"); + TopKNode topKNode = createTopK(topKId, SortOrder.DESC_NULLS_LAST); + topKNode.addChild(new ExchangeNode(new PlanNodeId("exchange"), null, null, null, null)); + + TopKNode optimized = (TopKNode) new TopKRuntimeFilterOptimizer().optimize(topKNode, null); + + Assert.assertFalse(optimized.isUseTopKRuntimeFilter()); + } + + private TopKNode createTopK(PlanNodeId id, SortOrder sortOrder) { + Symbol timeSymbol = new Symbol("time"); + OrderingScheme orderingScheme = + new OrderingScheme(Collections.singletonList(timeSymbol), sortOrder); + return new TopKNode(id, orderingScheme, 10, Collections.singletonList(timeSymbol), false); + } + + private DeviceTableScanNode createScan(PlanNodeId id) { + return new DeviceTableScanNode( + id, null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); + } +} 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..70c866d5e0d 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 @@ -50,6 +50,13 @@ public class TopKNode extends MultiChildProcessNode { private final boolean childrenDataInOrder; + // Marked during distributed optimize when TopK runtime filter should be generated. + private boolean useTopKRuntimeFilter; + + // Sort direction of the runtime filter threshold; meaningful only when useTopKRuntimeFilter is + // true. + private boolean topKRuntimeFilterAscending = true; + public TopKNode( PlanNodeId id, OrderingScheme scheme, @@ -79,7 +86,11 @@ public class TopKNode extends MultiChildProcessNode { @Override public PlanNode clone() { - return new TopKNode(getPlanNodeId(), orderingScheme, count, outputSymbols, childrenDataInOrder); + TopKNode cloned = + new TopKNode(getPlanNodeId(), orderingScheme, count, outputSymbols, childrenDataInOrder); + cloned.useTopKRuntimeFilter = useTopKRuntimeFilter; + cloned.topKRuntimeFilterAscending = topKRuntimeFilterAscending; + return cloned; } @Override @@ -102,6 +113,8 @@ public class TopKNode extends MultiChildProcessNode { Symbol.serialize(symbol, byteBuffer); } ReadWriteIOUtils.write(childrenDataInOrder, byteBuffer); + ReadWriteIOUtils.write(useTopKRuntimeFilter, byteBuffer); + ReadWriteIOUtils.write(topKRuntimeFilterAscending, byteBuffer); } @Override @@ -114,6 +127,8 @@ public class TopKNode extends MultiChildProcessNode { Symbol.serialize(symbol, stream); } ReadWriteIOUtils.write(childrenDataInOrder, stream); + ReadWriteIOUtils.write(useTopKRuntimeFilter, stream); + ReadWriteIOUtils.write(topKRuntimeFilterAscending, stream); } public static TopKNode deserialize(ByteBuffer byteBuffer) { @@ -125,8 +140,14 @@ public class TopKNode extends MultiChildProcessNode { outputSymbols.add(Symbol.deserialize(byteBuffer)); } boolean childrenDataInOrder = ReadWriteIOUtils.readBool(byteBuffer); + boolean useTopKRuntimeFilter = ReadWriteIOUtils.readBool(byteBuffer); + boolean topKRuntimeFilterAscending = ReadWriteIOUtils.readBool(byteBuffer); PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer); - return new TopKNode(planNodeId, orderingScheme, count, outputSymbols, childrenDataInOrder); + TopKNode topKNode = + new TopKNode(planNodeId, orderingScheme, count, outputSymbols, childrenDataInOrder); + topKNode.useTopKRuntimeFilter = useTopKRuntimeFilter; + topKNode.topKRuntimeFilterAscending = topKRuntimeFilterAscending; + return topKNode; } @Override @@ -139,7 +160,11 @@ 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.useTopKRuntimeFilter = useTopKRuntimeFilter; + topKNode.topKRuntimeFilterAscending = topKRuntimeFilterAscending; + return topKNode; } public OrderingScheme getOrderingScheme() { @@ -154,6 +179,22 @@ public class TopKNode extends MultiChildProcessNode { return childrenDataInOrder; } + public boolean isUseTopKRuntimeFilter() { + return useTopKRuntimeFilter; + } + + public void setUseTopKRuntimeFilter(boolean useTopKRuntimeFilter) { + this.useTopKRuntimeFilter = useTopKRuntimeFilter; + } + + public boolean isTopKRuntimeFilterAscending() { + return topKRuntimeFilterAscending; + } + + public void setTopKRuntimeFilterAscending(boolean topKRuntimeFilterAscending) { + this.topKRuntimeFilterAscending = topKRuntimeFilterAscending; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -162,12 +203,20 @@ 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) + && useTopKRuntimeFilter == sortNode.useTopKRuntimeFilter + && topKRuntimeFilterAscending == sortNode.topKRuntimeFilterAscending; } @Override public int hashCode() { - return Objects.hashCode(super.hashCode(), orderingScheme, outputSymbols, count); + return Objects.hashCode( + super.hashCode(), + orderingScheme, + outputSymbols, + count, + useTopKRuntimeFilter, + topKRuntimeFilterAscending); } @Override
