This is an automated email from the ASF dual-hosted git repository. Wei-hao-Li pushed a commit to branch deviceEntrySpill-devPr in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 99f76a0a3cf5148ff44c615b6380c39cc16bbd90 Author: Weihao Li <[email protected]> AuthorDate: Tue Aug 18 23:52:14 2026 +0800 fix & add IT --- .../it/env/cluster/config/MppDataNodeConfig.java | 6 + .../it/env/remote/config/RemoteDataNodeConfig.java | 5 + .../apache/iotdb/itbase/env/DataNodeConfig.java | 2 + .../it/query/recent/IoTDBDeviceEntrySpillIT.java | 143 ++++++++++ .../iotdb/db/i18n/DataNodeQueryMessages.java | 2 + .../iotdb/db/i18n/DataNodeQueryMessages.java | 2 + .../fragment/FragmentInstanceContext.java | 15 +- .../queryengine/plan/execution/QueryExecution.java | 12 + .../metadata/fetcher/DeviceEntryFetchContext.java | 71 +++++ .../metadata/fetcher/TableDeviceSchemaFetcher.java | 298 ++++++++++----------- .../spill/AbstractDeviceEntryMaterializer.java | 33 ++- .../metadata/spill/DeviceEntryMaterializer.java | 22 +- .../spill/DeviceEntrySortedMaterializer.java | 88 +++++- .../distribute/TableDistributedPlanGenerator.java | 76 +++++- .../relational/sql/ast/AbstractTraverseDevice.java | 5 +- .../spill/DeviceEntryMaterializerTest.java | 27 ++ 16 files changed, 611 insertions(+), 196 deletions(-) diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java index 8399955a5c5..8c5f56ad9f1 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java @@ -180,4 +180,10 @@ public class MppDataNodeConfig extends MppBaseConfig implements DataNodeConfig { setProperty("dn_multi_dir_strategy", multiDirStrategy); return this; } + + @Override + public DataNodeConfig setTableQueryDeviceEntryBatchSizeInBytes(long batchSizeInBytes) { + setProperty("table_query_device_entry_batch_size_in_bytes", String.valueOf(batchSizeInBytes)); + return this; + } } diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java index a76608e4851..c97e2b5065a 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java @@ -125,4 +125,9 @@ public class RemoteDataNodeConfig implements DataNodeConfig { public DataNodeConfig setDnMultiDirStrategy(String multiDirStrategy) { return this; } + + @Override + public DataNodeConfig setTableQueryDeviceEntryBatchSizeInBytes(long batchSizeInBytes) { + return this; + } } diff --git a/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java b/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java index bc045c9ba2f..fb969c778ea 100644 --- a/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java @@ -65,4 +65,6 @@ public interface DataNodeConfig { DataNodeConfig setDnDataDirs(String dnDataDirs); DataNodeConfig setDnMultiDirStrategy(String multiDirStrategy); + + DataNodeConfig setTableQueryDeviceEntryBatchSizeInBytes(long batchSizeInBytes); } diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBDeviceEntrySpillIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBDeviceEntrySpillIT.java new file mode 100644 index 00000000000..39bfbb9dd0a --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBDeviceEntrySpillIT.java @@ -0,0 +1,143 @@ +/* + * 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.itbase.category.TableClusterIT; +import org.apache.iotdb.itbase.category.TableLocalStandaloneIT; +import org.apache.iotdb.itbase.env.BaseEnv; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; + +@Category({TableLocalStandaloneIT.class, TableClusterIT.class}) +public class IoTDBDeviceEntrySpillIT { + + @BeforeClass + public static void setUp() throws Exception { + EnvFactory.getEnv().getConfig().getDataNodeConfig().setTableQueryDeviceEntryBatchSizeInBytes(1); + EnvFactory.getEnv().initClusterEnvironment(); + try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = connection.createStatement()) { + statement.execute("CREATE DATABASE spill_test"); + statement.execute( + "CREATE TABLE spill_test.device_data (tag1 STRING TAG, tag2 STRING TAG, " + + "value INT32 FIELD)"); + statement.execute( + "INSERT INTO spill_test.device_data(tag1, tag2, time, value) " + + "VALUES ('a', 'x', 1, 10), ('a', 'x', 2, 20), " + + "('b', 'y', 1, 30), ('c', 'z', 1, 40)"); + } + } + + @AfterClass + public static void tearDown() throws Exception { + EnvFactory.getEnv().cleanClusterEnvironment(); + } + + @Test + public void testRawFullTableQueryWithSpill() throws Exception { + try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = connection.createStatement(); + ResultSet resultSet = + statement.executeQuery("SELECT tag1, tag2, value FROM spill_test.device_data")) { + int rowCount = 0; + while (resultSet.next()) { + rowCount++; + } + assertEquals(4, rowCount); + } + } + + @Test + public void testRawQueriesWithTimeFilterProjectionFilterLimitAndOrdering() throws Exception { + String[] queries = { + "SELECT * FROM spill_test.device_data WHERE time >= 1 AND time < 3", + "SELECT time, value FROM spill_test.device_data WHERE time >= 1 AND time < 3", + "SELECT tag1, tag2, value FROM spill_test.device_data " + + "WHERE time >= 1 AND time < 3 AND value > 10", + "SELECT * FROM spill_test.device_data WHERE time >= 1 AND time < 3 LIMIT 2", + "SELECT * FROM spill_test.device_data WHERE time >= 1 AND time < 3 ORDER BY time ASC", + "SELECT * FROM spill_test.device_data WHERE time >= 1 AND time < 3 " + "ORDER BY tag1, time" + }; + for (String query : queries) { + assertRowCount(query, query.contains("LIMIT 2") ? 2 : query.contains("value > 10") ? 2 : 4); + } + } + + @Test + public void testAggregationQueryWithSpill() throws Exception { + try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = connection.createStatement(); + ResultSet resultSet = + statement.executeQuery("SELECT count(value) FROM spill_test.device_data")) { + assertEquals(true, resultSet.next()); + assertEquals(4, resultSet.getLong(1)); + assertEquals(false, resultSet.next()); + } + } + + @Test + public void testGroupedAggregationAcrossSpillSegments() throws Exception { + assertRowCount("SELECT tag1, count(*) FROM spill_test.device_data GROUP BY tag1", 3); + assertRowCount( + "SELECT tag1, tag2, count(*), sum(value) FROM spill_test.device_data " + + "GROUP BY tag1, tag2", + 3); + assertRowCount( + "SELECT date_bin(1s, time), count(*) FROM spill_test.device_data " + + "GROUP BY date_bin(1s, time)", + 2); + } + + @Test + public void testOrPredicateDoesNotDuplicateDeviceRows() throws Exception { + try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = connection.createStatement(); + ResultSet resultSet = + statement.executeQuery( + "SELECT count(*) FROM spill_test.device_data " + + "WHERE tag1 = 'a' OR tag2 = 'x'")) { + assertEquals(true, resultSet.next()); + assertEquals(2, resultSet.getLong(1)); + assertEquals(false, resultSet.next()); + } + } + + private void assertRowCount(String sql, int expectedRowCount) throws Exception { + try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + int rowCount = 0; + while (resultSet.next()) { + rowCount++; + } + assertEquals(expectedRowCount, rowCount); + } + } +} diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 5cb613debef..6821e295157 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -3821,5 +3821,7 @@ public final class DataNodeQueryMessages { public static final String LOG_FAILED_TO_CLEAN_DEVICEENTRY_DATA_SET_ASYNCHRONOUSLY_QUERYID_ARG_PLANNODEID_ARG_9106C4C5 = "Failed to clean DeviceEntry data set asynchronously: queryId=%s, planNodeId=%s"; + public static final String LOG_FAILED_TO_CLEAN_DEVICEENTRY_SPILL_DIRECTORY_QUERYID_ARG_ADF95D63 = + "Failed to clean DeviceEntry spill directory for query %s"; } diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 89bafbd34ed..b1ec4c71a93 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -4578,5 +4578,7 @@ public final class DataNodeQueryMessages { public static final String LOG_FAILED_TO_CLEAN_DEVICEENTRY_DATA_SET_ASYNCHRONOUSLY_QUERYID_ARG_PLANNODEID_ARG_9106C4C5 = "异步清理 DeviceEntry 数据集失败:queryId=%s,planNodeId=%s"; + public static final String LOG_FAILED_TO_CLEAN_DEVICEENTRY_SPILL_DIRECTORY_QUERYID_ARG_ADF95D63 = + "清理 query %s 的 DeviceEntry spill 目录失败"; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java index ad363016808..1a0af9bb579 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java @@ -819,14 +819,13 @@ public class FragmentInstanceContext extends QueryContext { return unfinishedResultSupplier.get(); } QueryDataSource dataSource = - (QueryDataSource) - dataRegion.query( - sourcePaths, - singleDeviceId, - this, - globalTimeFilter != null ? globalTimeFilter.copy() : null, - timePartitions, - waitForLockTime); + dataRegion.query( + sourcePaths, + singleDeviceId, + this, + globalTimeFilter != null ? globalTimeFilter.copy() : null, + timePartitions, + waitForLockTime); if (dataSource == null) { return unfinishedResultSupplier.get(); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java index 035bac7b5d8..18578e625fd 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java @@ -48,6 +48,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan; import org.apache.iotdb.db.queryengine.plan.planner.plan.FragmentInstance; import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeUtil; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntrySpillManager; import org.apache.iotdb.db.queryengine.plan.scheduler.IScheduler; import org.apache.iotdb.db.utils.SetThreadName; import org.apache.iotdb.mpp.rpc.thrift.TFragmentInstanceId; @@ -437,6 +438,17 @@ public class QueryExecution implements IQueryExecution { cleanUpResultHandle(); } context.releaseExternalTsFileQueryResources(); + if (t != null) { + try { + DeviceEntrySpillManager.getInstance().deregisterQuery(context.getQueryId().getId()); + } catch (Exception e) { + LOGGER.warn( + DataNodeQueryMessages + .LOG_FAILED_TO_CLEAN_DEVICEENTRY_SPILL_DIRECTORY_QUERYID_ARG_ADF95D63, + context.getQueryId().getId(), + e); + } + } } /** diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/DeviceEntryFetchContext.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/DeviceEntryFetchContext.java new file mode 100644 index 00000000000..47c01459154 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/DeviceEntryFetchContext.java @@ -0,0 +1,71 @@ +/* + * 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.metadata.fetcher; + +import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.AbstractDeviceEntryMaterializer; + +/** Query-scoped state shared while fetching and materializing DeviceEntry objects. */ +public final class DeviceEntryFetchContext { + + private final MPPQueryContext queryContext; + private final PlanNodeId planNodeId; + private AbstractDeviceEntryMaterializer materializer; + private boolean mayContainDuplicateDevice; + private boolean containsNonAlignedDevice; + + public DeviceEntryFetchContext(final MPPQueryContext queryContext, final PlanNodeId planNodeId) { + this.queryContext = queryContext; + this.planNodeId = planNodeId; + } + + public MPPQueryContext getQueryContext() { + return queryContext; + } + + public PlanNodeId getPlanNodeId() { + return planNodeId; + } + + public AbstractDeviceEntryMaterializer getMaterializer() { + return materializer; + } + + public void setMaterializer(final AbstractDeviceEntryMaterializer materializer) { + this.materializer = materializer; + } + + public boolean mayContainDuplicateDevice() { + return mayContainDuplicateDevice; + } + + public void setMayContainDuplicateDevice(final boolean mayContainDuplicateDevice) { + this.mayContainDuplicateDevice = mayContainDuplicateDevice; + } + + public boolean containsNonAlignedDevice() { + return containsNonAlignedDevice; + } + + public void markContainsNonAlignedDevice() { + containsNonAlignedDevice = true; + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaFetcher.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaFetcher.java index 1ac6e1789a4..32fcfdc546c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaFetcher.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaFetcher.java @@ -50,9 +50,11 @@ import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.De import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.IDeviceSchema; import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.TableDeviceSchemaCache; import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.TreeDeviceNormalSchema; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.AbstractDeviceEntryMaterializer; import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntryDataSet; import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntryDataSetResult; import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntryMaterializer; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntrySortedMaterializer; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AbstractTraverseDevice; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.FetchDevice; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ShowDevice; @@ -74,6 +76,7 @@ import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -228,7 +231,7 @@ public class TableDeviceSchemaFetcher { final MPPQueryContext queryContext) { final Map<String, List<DeviceEntry>> deviceEntryMap = new HashMap<>(); final TsTable tableInstance = DataNodeTableCache.getInstance().getTable(database, table); - final AtomicBoolean mayContainDuplicateDevice = new AtomicBoolean(false); + final DeviceEntryFetchContext fetchContext = new DeviceEntryFetchContext(queryContext, null); if (!TreeViewSchema.isTreeViewTable(tableInstance)) { deviceEntryMap.put(database, new ArrayList<>()); } @@ -241,17 +244,16 @@ public class TableDeviceSchemaFetcher { statement, deviceEntryMap, attributeColumns, - queryContext, - mayContainDuplicateDevice, + fetchContext, false)) { fetchMissingDeviceSchemaForQuery( - database, tableInstance, attributeColumns, statement, deviceEntryMap, null, queryContext); + database, tableInstance, attributeColumns, statement, deviceEntryMap, fetchContext); } // TODO table metadata: implement deduplicate during schemaRegion execution // TODO table metadata: need further process on input predicates and transform them into // disjoint sets - return mayContainDuplicateDevice.get() + return fetchContext.mayContainDuplicateDevice() ? deviceEntryMap.entrySet().stream() .collect( Collectors.toMap( @@ -268,119 +270,79 @@ public class TableDeviceSchemaFetcher { final MPPQueryContext queryContext, final PlanNodeId planNodeId) { final TsTable tableInstance = DataNodeTableCache.getInstance().getTable(database, table); - if (TreeViewSchema.isTreeViewTable(tableInstance)) { - final Map<String, List<DeviceEntry>> deviceEntryMap = new HashMap<>(); - final AtomicBoolean mayContainDuplicateDevice = new AtomicBoolean(false); - boolean containsNonAlignedDevice = false; - final ShowDevice statement = new ShowDevice(database, table); - try (DeviceEntryMaterializer materializer = - new DeviceEntryMaterializer( - queryContext.getQueryId().getId(), - planNodeId, - CONFIG.getTableQueryDeviceEntryBatchSizeInBytes(), - true, - queryContext)) { - final boolean needRemoteFetch = - parseFilter4TraverseDevice( - tableInstance, - expressionList, - statement, - deviceEntryMap, - attributeColumns, - queryContext, - mayContainDuplicateDevice, - false); - for (List<DeviceEntry> entries : deviceEntryMap.values()) { - for (DeviceEntry entry : entries) { - appendToMaterializer(materializer, entry, queryContext, true); - if (entry instanceof NonAlignedDeviceEntry) { - containsNonAlignedDevice = true; - } - } - entries.clear(); - } - if (needRemoteFetch) { - containsNonAlignedDevice |= - fetchMissingDeviceSchemaForQuery( - database, - tableInstance, - attributeColumns, - statement, - deviceEntryMap, - materializer, - queryContext); - } - if (deviceEntryMap.size() > 1) { - throw new SemanticException( - DataNodeQueryMessages.TREE_DEVICE_VIEW_WITH_MULTIPLE_DATABASES - + deviceEntryMap.keySet() - + DataNodeQueryMessages.IS_UNSUPPORTED_YET); - } - final String resultDatabase = - deviceEntryMap.isEmpty() ? null : deviceEntryMap.keySet().iterator().next(); - return new DeviceEntryDataSetResult( - resultDatabase, materializer.finish(), containsNonAlignedDevice); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - final Map<String, List<DeviceEntry>> cachedEntries = new HashMap<>(); - cachedEntries.put(database, new ArrayList<>()); - final AtomicBoolean mayContainDuplicateDevice = new AtomicBoolean(false); + final DeviceEntryFetchContext fetchContext = + new DeviceEntryFetchContext(queryContext, planNodeId); final ShowDevice statement = new ShowDevice(database, table); - final boolean needRemoteFetch = - parseFilter4TraverseDevice( - tableInstance, - expressionList, - statement, - cachedEntries, - attributeColumns, - queryContext, - mayContainDuplicateDevice, - false); - - if (mayContainDuplicateDevice.get()) { + final Map<String, List<DeviceEntry>> deviceEntryMap = new HashMap<>(); + if (!TreeViewSchema.isTreeViewTable(tableInstance)) { + deviceEntryMap.put(database, new ArrayList<>()); + } + Throwable failure = null; + try { + final boolean needRemoteFetch = + parseFilter4TraverseDevice( + tableInstance, + expressionList, + statement, + deviceEntryMap, + attributeColumns, + fetchContext, + false); + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); if (needRemoteFetch) { fetchMissingDeviceSchemaForQuery( - database, - tableInstance, - attributeColumns, - statement, - cachedEntries, - null, - queryContext); + database, tableInstance, attributeColumns, statement, deviceEntryMap, fetchContext); } - cachedEntries.put( - database, new ArrayList<>(new LinkedHashSet<>(cachedEntries.get(database)))); - } - - try (DeviceEntryMaterializer materializer = - new DeviceEntryMaterializer( - queryContext.getQueryId().getId(), - planNodeId, - CONFIG.getTableQueryDeviceEntryBatchSizeInBytes(), - true, - queryContext)) { - for (DeviceEntry entry : cachedEntries.get(database)) { - appendToMaterializer(materializer, entry, queryContext, true); + if (deviceEntryMap.size() > 1) { + throw new SemanticException( + DataNodeQueryMessages.TREE_DEVICE_VIEW_WITH_MULTIPLE_DATABASES + + deviceEntryMap.keySet() + + DataNodeQueryMessages.IS_UNSUPPORTED_YET); } - cachedEntries.get(database).clear(); - if (needRemoteFetch && !mayContainDuplicateDevice.get()) { - fetchMissingDeviceSchemaForQuery( - database, - tableInstance, - attributeColumns, - statement, - cachedEntries, - materializer, - queryContext); - } - final DeviceEntryDataSet dataSet = materializer.finish(); - return new DeviceEntryDataSetResult(database, dataSet, false); + + DeviceEntryDataSet dataSet; + dataSet = materializer.finish(); + + return new DeviceEntryDataSetResult( + deviceEntryMap.isEmpty() ? null : deviceEntryMap.keySet().iterator().next(), + dataSet, + fetchContext.containsNonAlignedDevice()); } catch (IOException e) { + failure = e; throw new UncheckedIOException(e); + } catch (Exception e) { + failure = e; + throw e; + } finally { + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); + if (materializer != null) { + try { + materializer.close(); + } catch (IOException e) { + if (failure != null) { + failure.addSuppressed(e); + } else { + throw new UncheckedIOException(e); + } + } + } + } + } + + private AbstractDeviceEntryMaterializer createDataSetMaterializer( + MPPQueryContext queryContext, PlanNodeId planNodeId, boolean distinct) { + long batchSize = CONFIG.getTableQueryDeviceEntryBatchSizeInBytes(); + if (distinct) { + return new DeviceEntrySortedMaterializer( + queryContext.getQueryId().getId(), + planNodeId, + batchSize, + Comparator.comparing(entry -> entry.getDeviceID().toString()), + true, + queryContext); } + return new DeviceEntryMaterializer( + queryContext.getQueryId().getId(), planNodeId, batchSize, true, queryContext); } // Used by show/count device and update device. @@ -391,9 +353,9 @@ public class TableDeviceSchemaFetcher { final AbstractTraverseDevice statement, final Map<String, List<DeviceEntry>> deviceEntryMap, final List<String> attributeColumns, - final MPPQueryContext queryContext, - final AtomicBoolean mayContainDuplicateDevice, + final DeviceEntryFetchContext fetchContext, final boolean isDirectDeviceQuery) { + final MPPQueryContext queryContext = fetchContext.getQueryContext(); final Pair<List<Expression>, List<Expression>> separatedExpression = SchemaPredicateUtil.separateTagDeterminedPredicate( expressionList, tableInstance, queryContext, isDirectDeviceQuery); @@ -405,9 +367,20 @@ public class TableDeviceSchemaFetcher { // Each element represents one batch of possible devices // expressions inner each element are and-concat representing conditions of different column + final AtomicBoolean mayContainDuplicateDevice = new AtomicBoolean(false); final List<Map<Integer, List<SchemaFilter>>> index2FilterMapList = SchemaPredicateUtil.convertTagPredicateToOrConcatList( tagDeterminedPredicateList, tableInstance, mayContainDuplicateDevice); + fetchContext.setMayContainDuplicateDevice(mayContainDuplicateDevice.get()); + + if (fetchContext.getPlanNodeId() != null) { + fetchContext.setMaterializer( + createDataSetMaterializer( + queryContext, + fetchContext.getPlanNodeId(), + fetchContext.mayContainDuplicateDevice())); + } + // If a predicate branch contains comparisons for all tag columns and can use SchemaCache, we // store its index. final List<Integer> tagSingleMatchIndexList = @@ -455,7 +428,7 @@ public class TableDeviceSchemaFetcher { attributeColumns, fetchPaths, isDirectDeviceQuery, - queryContext)) { + fetchContext)) { tagSingleMatchPredicateNotInCache.add(index); } } @@ -521,7 +494,7 @@ public class TableDeviceSchemaFetcher { final List<String> attributeColumns, final List<IDeviceID> fetchPaths, final boolean isDirectDeviceQuery, - final MPPQueryContext queryContext) { + final DeviceEntryFetchContext fetchContext) { final String[] tagValues = new String[tableInstance.getTagNum()]; for (final List<SchemaFilter> schemaFilters : tagFilters.values()) { final TagFilter tagFilter = (TagFilter) schemaFilters.get(0); @@ -539,8 +512,9 @@ public class TableDeviceSchemaFetcher { fetchPaths, isDirectDeviceQuery, tagValues, - queryContext) - : tryGetTreeDeviceInCache(deviceEntryMap, tableInstance, check, fetchPaths, tagValues); + fetchContext) + : tryGetTreeDeviceInCache( + deviceEntryMap, tableInstance, check, fetchPaths, tagValues, fetchContext); } private boolean tryGetTableDeviceInCache( @@ -552,7 +526,9 @@ public class TableDeviceSchemaFetcher { final List<IDeviceID> fetchPaths, final boolean isDirectDeviceQuery, final String[] tagValues, - final MPPQueryContext queryContext) { + final DeviceEntryFetchContext fetchContext) { + final MPPQueryContext queryContext = fetchContext.getQueryContext(); + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); final IDeviceID deviceID = convertTagValuesToDeviceID(tableInstance.getTableName(), tagValues); final Map<String, Binary> attributeMap = cache.getDeviceAttribute(database, deviceID); @@ -572,13 +548,16 @@ public class TableDeviceSchemaFetcher { // TODO table metadata: process cases that selected attr columns different from those used for // predicate if (check.test(deviceEntry)) { - deviceEntryList.add(deviceEntry); + queryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); + if (materializer == null) { + deviceEntryList.add(deviceEntry); + } else { + appendToMaterializer(materializer, deviceEntry, queryContext); + } // If we partially hit cache in direct device query, we must fetch for all the predicates // because now we do not support combining memory source and other sources if (isDirectDeviceQuery) { fetchPaths.add(deviceID); - } else { - queryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); } } return true; @@ -589,7 +568,10 @@ public class TableDeviceSchemaFetcher { final TsTable tableInstance, final Predicate<AlignedDeviceEntry> check, final List<IDeviceID> fetchPaths, - final String[] tagValues) { + final String[] tagValues, + final DeviceEntryFetchContext fetchContext) { + final MPPQueryContext queryContext = fetchContext.getQueryContext(); + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); final IDeviceID deviceID = DataNodeTreeViewSchemaUtils.convertToIDeviceID(tableInstance, tagValues); final IDeviceSchema schema = TableDeviceSchemaCache.getInstance().getDeviceSchema(deviceID); @@ -601,12 +583,19 @@ public class TableDeviceSchemaFetcher { return false; } database = ((TreeDeviceNormalSchema) schema).getDatabase(); - deviceEntryMap - .computeIfAbsent(database, k -> new ArrayList<>()) - .add( - ((TreeDeviceNormalSchema) schema).isAligned() - ? new AlignedDeviceEntry(deviceID, new Binary[0]) - : new NonAlignedDeviceEntry(deviceID, new Binary[0])); + final DeviceEntry deviceEntry = + ((TreeDeviceNormalSchema) schema).isAligned() + ? new AlignedDeviceEntry(deviceID, new Binary[0]) + : new NonAlignedDeviceEntry(deviceID, new Binary[0]); + queryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); + if (materializer == null) { + deviceEntryMap.computeIfAbsent(database, k -> new ArrayList<>()).add(deviceEntry); + } else { + appendToMaterializer(materializer, deviceEntry, queryContext); + } + if (deviceEntry instanceof NonAlignedDeviceEntry) { + fetchContext.markContainsNonAlignedDevice(); + } return true; } @@ -619,16 +608,16 @@ public class TableDeviceSchemaFetcher { return IDeviceID.Factory.DEFAULT_FACTORY.create(deviceIdNodes); } - private boolean fetchMissingDeviceSchemaForQuery( + private void fetchMissingDeviceSchemaForQuery( final String database, final TsTable tableInstance, final List<String> attributeColumns, final ShowDevice statement, final Map<String, List<DeviceEntry>> deviceEntryMap, - final DeviceEntryMaterializer materializer, - final MPPQueryContext mppQueryContext) { + final DeviceEntryFetchContext fetchContext) { Throwable t = null; - boolean containsNonAlignedDevice = false; + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); + final MPPQueryContext mppQueryContext = fetchContext.getQueryContext(); final long queryId = SessionManager.getInstance().requestQueryId(); // For the correctness of attribute remote update @@ -702,14 +691,8 @@ public class TableDeviceSchemaFetcher { deviceEntryMap.get(database), materializer); } else { - containsNonAlignedDevice |= - constructTreeResults( - tsBlock.get(), - columnHeaderList, - tableInstance, - mppQueryContext, - deviceEntryMap, - materializer); + constructTreeResults( + tsBlock.get(), columnHeaderList, tableInstance, deviceEntryMap, fetchContext); } } } else { @@ -722,7 +705,6 @@ public class TableDeviceSchemaFetcher { TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); } } - return containsNonAlignedDevice; } catch (final Throwable throwable) { t = throwable; throw throwable; @@ -743,7 +725,7 @@ public class TableDeviceSchemaFetcher { final MPPQueryContext mppQueryContext, final List<String> attributeColumns, final List<DeviceEntry> deviceEntryList, - final DeviceEntryMaterializer materializer) { + final AbstractDeviceEntryMaterializer materializer) { final Column[] columns = tsBlock.getValueColumns(); for (int i = 0; i < tsBlock.getPositionCount(); i++) { final String[] nodes = new String[tableInstance.getTagNum() + 1]; @@ -760,11 +742,11 @@ public class TableDeviceSchemaFetcher { final AlignedDeviceEntry deviceEntry = new AlignedDeviceEntry( deviceID, attributeColumns.stream().map(attributeMap::get).toArray(Binary[]::new)); + mppQueryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); if (materializer == null) { - mppQueryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); deviceEntryList.add(deviceEntry); } else { - appendToMaterializer(materializer, deviceEntry, mppQueryContext, false); + appendToMaterializer(materializer, deviceEntry, mppQueryContext); } // Only cache those exact device query // Fetch paths is null iff there are fuzzy queries related to id columns @@ -775,33 +757,27 @@ public class TableDeviceSchemaFetcher { } private static void appendToMaterializer( - DeviceEntryMaterializer materializer, + AbstractDeviceEntryMaterializer materializer, DeviceEntry deviceEntry, - MPPQueryContext queryContext, - boolean memoryAlreadyReserved) { + MPPQueryContext queryContext) { try { long releasedRamBytes = materializer.appendWithMemoryControl(deviceEntry); if (releasedRamBytes > 0) { queryContext.releaseMemoryReservedForFrontEnd(releasedRamBytes); } - if (memoryAlreadyReserved && materializer.isSpilled()) { - queryContext.releaseMemoryReservedForFrontEnd(deviceEntry.ramBytesUsed()); - } else if (!memoryAlreadyReserved && !materializer.isSpilled()) { - queryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); - } } catch (IOException e) { throw new UncheckedIOException(e); } } - private boolean constructTreeResults( + private void constructTreeResults( final TsBlock tsBlock, final List<ColumnHeader> columnHeaderList, final TsTable tableInstance, - final MPPQueryContext mppQueryContext, final Map<String, List<DeviceEntry>> deviceEntryMap, - final DeviceEntryMaterializer materializer) { - boolean containsNonAlignedDevice = false; + final DeviceEntryFetchContext fetchContext) { + final MPPQueryContext mppQueryContext = fetchContext.getQueryContext(); + final AbstractDeviceEntryMaterializer materializer = fetchContext.getMaterializer(); final Column[] columns = tsBlock.getValueColumns(); for (int i = 0; i < tsBlock.getPositionCount(); i++) { final String[] nodes = new String[tableInstance.getTagNum()]; @@ -809,23 +785,25 @@ public class TableDeviceSchemaFetcher { Collections.emptyMap(), nodes, null, columnHeaderList, columns, tableInstance, i); final IDeviceID deviceID = DataNodeTreeViewSchemaUtils.convertToIDeviceID(tableInstance, nodes); + final boolean isAligned = columns[columns.length - 2].getBoolean(i); final DeviceEntry deviceEntry = - columns[columns.length - 2].getBoolean(i) + isAligned ? new AlignedDeviceEntry(deviceID, new Binary[0]) : new NonAlignedDeviceEntry(deviceID, new Binary[0]); - containsNonAlignedDevice |= deviceEntry instanceof NonAlignedDeviceEntry; + if (!isAligned) { + fetchContext.markContainsNonAlignedDevice(); + } + mppQueryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); final List<DeviceEntry> deviceEntries = deviceEntryMap.computeIfAbsent( columns[columns.length - 1].getBinary(i).getStringValue(TSFileConfig.STRING_CHARSET), k -> new ArrayList<>()); if (materializer == null) { - mppQueryContext.reserveMemoryForFrontEnd(deviceEntry.ramBytesUsed()); deviceEntries.add(deviceEntry); } else { - appendToMaterializer(materializer, deviceEntry, mppQueryContext, false); + appendToMaterializer(materializer, deviceEntry, mppQueryContext); } } - return containsNonAlignedDevice; } private void constructNodesArrayAndAttributeMap( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/AbstractDeviceEntryMaterializer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/AbstractDeviceEntryMaterializer.java index 92886b0036a..e2b5e4f70f6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/AbstractDeviceEntryMaterializer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/AbstractDeviceEntryMaterializer.java @@ -36,9 +36,10 @@ public abstract class AbstractDeviceEntryMaterializer implements AutoCloseable { private final String queryId; private final PlanNodeId planNodeId; private final long thresholdInBytes; - private final List<DeviceEntry> bufferedEntries = new ArrayList<>(); + private List<DeviceEntry> bufferedEntries = new ArrayList<>(); private int entryCount; + private long bufferedRamBytes; private Path ownerDirectory; private boolean ownerRegistered; private boolean finished; @@ -60,6 +61,19 @@ public abstract class AbstractDeviceEntryMaterializer implements AutoCloseable { */ public abstract void append(DeviceEntry entry) throws IOException; + /** + * Appends a DeviceEntry while controlling this materializer's in-memory buffer. + * + * @return RAM bytes released when buffered entries are spilled + */ + public abstract long appendWithMemoryControl(DeviceEntry entry) throws IOException; + + public abstract boolean isSpilled(); + + public final long getBufferedRamBytes() { + return bufferedRamBytes; + } + public abstract DeviceEntryDataSet finish() throws IOException; protected final String queryId() { @@ -75,6 +89,14 @@ public abstract class AbstractDeviceEntryMaterializer implements AutoCloseable { entryCount++; } + protected final void addBufferedRamBytes(long ramBytes) { + bufferedRamBytes += ramBytes; + } + + protected final void clearBufferedRamBytes() { + bufferedRamBytes = 0; + } + protected final void incrementEntryCount() { entryCount++; } @@ -91,6 +113,11 @@ public abstract class AbstractDeviceEntryMaterializer implements AutoCloseable { return new ArrayList<>(bufferedEntries); } + protected final void replaceBufferedEntries(List<DeviceEntry> entries) { + bufferedEntries = entries; + entryCount = entries.size(); + } + protected final void sortBufferedEntries(Comparator<DeviceEntry> comparator) { bufferedEntries.sort(comparator); } @@ -116,6 +143,10 @@ public abstract class AbstractDeviceEntryMaterializer implements AutoCloseable { return entryCount; } + protected final void setEntryCount(int entryCount) { + this.entryCount = entryCount; + } + protected final void clearBuffer() { bufferedEntries.clear(); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializer.java index 82f53bb2caf..950c34d22f3 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializer.java @@ -30,8 +30,6 @@ public final class DeviceEntryMaterializer extends AbstractDeviceEntryMaterializ private final boolean rawSegment; private DeviceEntryDiskSpiller spiller; - // Only be used in fetchDeviceSchema, manages memory itself - private long rawBufferedRamBytes; public DeviceEntryMaterializer( String queryId, PlanNodeId planNodeId, long thresholdInBytes, boolean rawSegment) { @@ -56,17 +54,20 @@ public final class DeviceEntryMaterializer extends AbstractDeviceEntryMaterializ } /** Returns the RAM bytes released when Coordinator Raw Fetch switches to spill mode. */ + @Override public long appendWithMemoryControl(DeviceEntry entry) throws IOException { checkNotFinished(); - long ramBytesUsed = entry.ramBytesUsed(); - if (spiller == null && rawBufferedRamBytes + ramBytesUsed <= thresholdInBytes()) { - appendToBuffer(entry); - rawBufferedRamBytes += ramBytesUsed; - return 0; + if (spiller == null) { + long ramBytesUsed = entry.ramBytesUsed(); + if (getBufferedRamBytes() + ramBytesUsed <= thresholdInBytes()) { + appendToBuffer(entry); + addBufferedRamBytes(ramBytesUsed); + return 0; + } } - long releasedRamBytes = rawBufferedRamBytes; + long releasedRamBytes = getBufferedRamBytes(); ensureSpiller(); - rawBufferedRamBytes = 0; + clearBufferedRamBytes(); spiller.append(entry.serializeToBytes()); incrementEntryCount(); return releasedRamBytes; @@ -78,9 +79,10 @@ public final class DeviceEntryMaterializer extends AbstractDeviceEntryMaterializ if (spiller == null && !isBufferEmpty()) { ensureSpiller(); } - rawBufferedRamBytes = 0; + clearBufferedRamBytes(); } + @Override public boolean isSpilled() { return spiller != null; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySortedMaterializer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySortedMaterializer.java index d135139f700..bc087ad6efb 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySortedMaterializer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySortedMaterializer.java @@ -38,6 +38,7 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate private static final int MAX_MERGE_FAN_IN = 32; private final Comparator<DeviceEntry> comparator; + private final boolean distinct; private final List<List<Path>> sortedRuns = new ArrayList<>(); private Path runDirectory; @@ -47,8 +48,18 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate PlanNodeId planNodeId, long bufferSizeInBytes, Comparator<DeviceEntry> comparator) { + this(queryId, planNodeId, bufferSizeInBytes, comparator, false); + } + + public DeviceEntrySortedMaterializer( + String queryId, + PlanNodeId planNodeId, + long bufferSizeInBytes, + Comparator<DeviceEntry> comparator, + boolean distinct) { super(queryId, planNodeId, bufferSizeInBytes); this.comparator = comparator; + this.distinct = distinct; } public DeviceEntrySortedMaterializer( @@ -61,12 +72,44 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate setQueryContext(queryContext); } + public DeviceEntrySortedMaterializer( + String queryId, + PlanNodeId planNodeId, + long bufferSizeInBytes, + Comparator<DeviceEntry> comparator, + boolean distinct, + MPPQueryContext queryContext) { + this(queryId, planNodeId, bufferSizeInBytes, comparator, distinct); + setQueryContext(queryContext); + } + @Override public void append(DeviceEntry entry) throws IOException { checkNotFinished(); appendToBuffer(entry); } + @Override + public long appendWithMemoryControl(DeviceEntry entry) throws IOException { + checkNotFinished(); + long entryRamBytes = entry.ramBytesUsed(); + if (!isBufferEmpty() && getBufferedRamBytes() + entryRamBytes > thresholdInBytes()) { + long releasedRamBytes = getBufferedRamBytes(); + flushRun(); + appendToBuffer(entry); + addBufferedRamBytes(entryRamBytes); + return releasedRamBytes; + } + appendToBuffer(entry); + addBufferedRamBytes(entryRamBytes); + return 0; + } + + @Override + public boolean isSpilled() { + return !sortedRuns.isEmpty(); + } + @Override public void forceSpill() throws IOException { checkNotFinished(); @@ -83,6 +126,9 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate } if (sortedRuns.isEmpty()) { sortBufferedEntries(comparator); + if (distinct) { + deduplicateBufferedEntries(); + } DeviceEntryDataSet dataSet = new InMemoryDeviceEntryDataSet(copyBufferedEntries()); markFinished(); return dataSet; @@ -93,15 +139,17 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate List<List<Path>> finalRuns = compactRuns(new ArrayList<>(sortedRuns)); Path finalDirectory = ownerDirectory().resolve("fi"); List<Path> finalSegments; + int finalEntryCount; try (DeviceEntryDiskSpiller outputSpiller = new DeviceEntryDiskSpiller(finalDirectory, thresholdInBytes(), ioContext())) { if (finalRuns.size() == 1) { - copyRun(finalRuns.get(0), outputSpiller); + finalEntryCount = copyRun(finalRuns.get(0), outputSpiller); } else { - mergeRuns(finalRuns, outputSpiller); + finalEntryCount = mergeRuns(finalRuns, outputSpiller); } finalSegments = outputSpiller.finish(); } + setEntryCount(finalEntryCount); DeviceEntryDataSet dataSet = new SpilledDeviceEntryDataSet(queryId(), ownerDirectory(), finalSegments, entryCount()); markFinished(); @@ -132,6 +180,7 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate sortedRuns.add(runSpiller.finish()); } clearBuffer(); + clearBufferedRamBytes(); } private void ensureSpillDirectory() throws IOException { @@ -142,13 +191,21 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate runDirectory = ensureOwnerDirectory().resolve("sort-run"); } - private void copyRun(List<Path> run, DeviceEntryDiskSpiller outputSpiller) throws IOException { + private int copyRun(List<Path> run, DeviceEntryDiskSpiller outputSpiller) throws IOException { + int outputCount = 0; + DeviceEntry previous = null; try (DeviceEntryFileSpillerReader reader = new DeviceEntryFileSpillerReader(run, true, ioContext())) { while (reader.hasNext()) { - outputSpiller.append(reader.next().serializeToBytes()); + DeviceEntry entry = reader.next(); + if (!distinct || previous == null || comparator.compare(previous, entry) != 0) { + outputSpiller.append(entry.serializeToBytes()); + previous = entry; + outputCount++; + } } } + return outputCount; } private void deleteRunDirectoryBestEffort() { @@ -186,7 +243,7 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate return runs; } - private void mergeRuns(List<List<Path>> runs, DeviceEntryDiskSpiller outputSpiller) + private int mergeRuns(List<List<Path>> runs, DeviceEntryDiskSpiller outputSpiller) throws IOException { List<DeviceEntryFileSpillerReader> readers = new ArrayList<>(runs.size()); PriorityQueue<MergeElement> queue = @@ -196,6 +253,8 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate return result != 0 ? result : Integer.compare(left.readerIndex, right.readerIndex); }); Throwable failure = null; + int outputCount = 0; + DeviceEntry previous = null; try { for (int i = 0; i < runs.size(); i++) { DeviceEntryFileSpillerReader reader = @@ -207,7 +266,11 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate } while (!queue.isEmpty()) { MergeElement element = queue.poll(); - outputSpiller.append(element.entry.serializeToBytes()); + if (!distinct || previous == null || comparator.compare(previous, element.entry) != 0) { + outputSpiller.append(element.entry.serializeToBytes()); + previous = element.entry; + outputCount++; + } DeviceEntryFileSpillerReader reader = readers.get(element.readerIndex); if (reader.hasNext()) { queue.add(new MergeElement(reader.next(), element.readerIndex)); @@ -237,6 +300,19 @@ public final class DeviceEntrySortedMaterializer extends AbstractDeviceEntryMate } } } + return outputCount; + } + + private void deduplicateBufferedEntries() { + List<DeviceEntry> distinctEntries = new ArrayList<>(); + DeviceEntry previous = null; + for (DeviceEntry entry : bufferedEntries()) { + if (previous == null || comparator.compare(previous, entry) != 0) { + distinctEntries.add(entry); + previous = entry; + } + } + replaceBufferedEntries(distinctEntries); } private static final class MergeElement { 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 7e4024efe87..623d6e3f7ae 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 @@ -1311,6 +1311,7 @@ public class TableDistributedPlanGenerator node.getTreeDBName(), node.getMeasurementColumnNameMap()); scanNode.setRegionReplicaSet(regionReplicaSet); + scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId()); pair.left = scanNode; } @@ -1333,6 +1334,7 @@ public class TableDistributedPlanGenerator node.getTreeDBName(), node.getMeasurementColumnNameMap()); scanNode.setRegionReplicaSet(regionReplicaSet); + scanNode.setTopKRuntimeFilterSourceId(node.getTopKRuntimeFilterSourceId()); pair.right = scanNode; } @@ -1723,9 +1725,10 @@ public class TableDistributedPlanGenerator // push down aggregation if the child of aggregation node only has the union Node if (childrenNodes.size() == 1) { node.setChild(childrenNodes.get(0)); + AggregationNode physicalAggregation = withRateFunctionInputOrdering(node, childOrdering); if (childrenNodes.get(0) instanceof UnionNode - && node.getAggregations().values().stream() + && physicalAggregation.getAggregations().values().stream() .noneMatch(aggregation -> aggregation.isDistinct() || aggregation.hasMask())) { UnionNode unionNode = (UnionNode) childrenNodes.get(0); List<PlanNode> children = unionNode.getChildren(); @@ -1746,7 +1749,8 @@ public class TableDistributedPlanGenerator } // 2. split the aggregation into partial and final - Pair<AggregationNode, AggregationNode> splitResult = split(node, symbolAllocator, queryId); + Pair<AggregationNode, AggregationNode> splitResult = + split(physicalAggregation, symbolAllocator, queryId); AggregationNode intermediate = splitResult.right; // 3. add the aggregation node above the project node @@ -1781,7 +1785,7 @@ public class TableDistributedPlanGenerator return Collections.singletonList(splitResult.left); } - return Collections.singletonList(node); + return Collections.singletonList(physicalAggregation); } // We cannot do multi-stage Aggregate if any aggregation-function is distinct. @@ -1789,10 +1793,12 @@ public class TableDistributedPlanGenerator // MarkDistinctNode will merge all data from different child. if (node.getAggregations().values().stream() .anyMatch(aggregation -> aggregation.isDistinct() || aggregation.hasMask())) { - node.setChild( + PlanNode physicalChild = mergeChildrenViaCollectOrMergeSort( - nodeOrderingMap.get(childrenNodes.get(0).getPlanNodeId()), childrenNodes)); - return Collections.singletonList(node); + nodeOrderingMap.get(childrenNodes.get(0).getPlanNodeId()), childrenNodes); + node.setChild(physicalChild); + return Collections.singletonList( + withRateFunctionInputOrdering(node, nodeOrderingMap.get(physicalChild.getPlanNodeId()))); } Pair<AggregationNode, AggregationNode> splitResult = split(node, symbolAllocator, queryId); AggregationNode intermediate = splitResult.right; @@ -1824,6 +1830,58 @@ public class TableDistributedPlanGenerator return Collections.singletonList(splitResult.left); } + private static AggregationNode withRateFunctionInputOrdering( + AggregationNode node, OrderingScheme childOrdering) { + Map<Symbol, AggregationNode.Aggregation> aggregations = new LinkedHashMap<>(); + node.getAggregations() + .forEach( + (symbol, aggregation) -> + aggregations.put( + symbol, + new AggregationNode.Aggregation( + aggregation.getResolvedFunction(), + aggregation.getArguments(), + aggregation.isDistinct(), + aggregation.getFilter(), + aggregation.getOrderingScheme(), + aggregation.getMask(), + isInputOrderedByTimeAscending( + aggregation, node.getStep(), node.getGroupingKeys(), childOrdering)))); + return AggregationNode.builderFrom(node).setAggregations(aggregations).build(); + } + + static boolean isInputOrderedByTimeAscending( + AggregationNode.Aggregation aggregation, + AggregationNode.Step step, + List<Symbol> groupingKeys, + OrderingScheme childOrdering) { + String functionName = aggregation.getResolvedFunction().getSignature().getName(); + if (step != SINGLE + || childOrdering == null + || aggregation.getArguments().size() < 2 + || !("rate".equalsIgnoreCase(functionName) + || "increase".equalsIgnoreCase(functionName) + || "irate".equalsIgnoreCase(functionName) + || "delta".equalsIgnoreCase(functionName))) { + return false; + } + + Symbol timeSymbol = Symbol.from(aggregation.getArguments().get(1)); + List<Symbol> orderBy = childOrdering.getOrderBy(); + int timeIndex = orderBy.indexOf(timeSymbol); + if (timeIndex < 0 || !childOrdering.getOrdering(timeSymbol).isAscending()) { + return false; + } + + Set<Symbol> groupingKeySet = new HashSet<>(groupingKeys); + for (int i = 0; i < timeIndex; i++) { + if (!groupingKeySet.contains(orderBy.get(i))) { + return false; + } + } + return true; + } + private boolean prefixMatched(OrderingScheme childOrdering, List<Symbol> preGroupedSymbols) { List<Symbol> orderKeys = childOrdering.getOrderBy(); if (orderKeys.size() < preGroupedSymbols.size()) { @@ -1848,7 +1906,8 @@ public class TableDistributedPlanGenerator return Collections.singletonList(node); } - if (node.getCoordinatorDeviceEntryDataSet().isSpilled()) { + if (node.getCoordinatorDeviceEntryDataSet() != null + && node.getCoordinatorDeviceEntryDataSet().isSpilled()) { return constructSpilledAggregationTableScanByRegionReplicaSet( node, context, dataPartition, dbName); } @@ -2113,7 +2172,8 @@ public class TableDistributedPlanGenerator node.getMeasurementColumnNameMap())); } - if (node.getCoordinatorDeviceEntryDataSet().isSpilled()) { + if (node.getCoordinatorDeviceEntryDataSet() != null + && node.getCoordinatorDeviceEntryDataSet().isSpilled()) { return constructSpilledAggregationTreeDeviceViewScanByRegionReplicaSet( node, context, dataPartition, dbName); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractTraverseDevice.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractTraverseDevice.java index 310da433060..d807d565cc2 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractTraverseDevice.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractTraverseDevice.java @@ -34,6 +34,7 @@ import org.apache.iotdb.commons.schema.filter.SchemaFilter; import org.apache.iotdb.commons.schema.table.TsTable; import org.apache.iotdb.db.queryengine.common.MPPQueryContext; import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.DeviceEntryFetchContext; import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.TableDeviceSchemaFetcher; import org.apache.iotdb.db.queryengine.plan.relational.planner.ir.ExtractCommonPredicatesExpressionRewriter; @@ -46,7 +47,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; import static org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AbstractQueryDeviceWithCache.getDeviceColumnHeaderList; @@ -158,8 +158,7 @@ public abstract class AbstractTraverseDevice extends Statement { this, entries, attributeColumns, - context, - new AtomicBoolean(false), + new DeviceEntryFetchContext(context, null), true); } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializerTest.java index 20e17d195fd..12b0ab43c2a 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializerTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryMaterializerTest.java @@ -196,6 +196,33 @@ public class DeviceEntryMaterializerTest { assertEquals(expected, actual); } + @Test + public void testDistinctSortedMaterializerDeduplicatesAcrossRuns() throws Exception { + List<DeviceEntry> expected = createEntries(20); + List<DeviceEntry> input = new ArrayList<>(expected); + input.addAll(expected); + Comparator<DeviceEntry> comparator = + Comparator.comparing(entry -> entry.getDeviceID().toString()); + try (DeviceEntrySortedMaterializer materializer = + new DeviceEntrySortedMaterializer( + "q-distinct", new PlanNodeId("scan-0"), 128, comparator, true)) { + for (DeviceEntry entry : input) { + materializer.appendWithMemoryControl(entry); + } + List<DeviceEntry> actual = new ArrayList<>(); + try (DeviceEntryDataSet dataSet = materializer.finish(); + DeviceEntryReader reader = dataSet.openReader()) { + assertTrue(dataSet.isSpilled()); + assertEquals(expected.size(), dataSet.getEntryCount()); + while (reader.hasNext()) { + actual.add(reader.next()); + } + } + expected.sort(comparator); + assertEquals(expected, actual); + } + } + private static List<DeviceEntry> createEntries(int count) { List<DeviceEntry> entries = new ArrayList<>(count); for (int i = 0; i < count; i++) {
