This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new f6a844d8191 Query: Optimize schema traversal for non-leading tag IN 
predicates (#18369)
f6a844d8191 is described below

commit f6a844d8191f4d7bf550c22dc5bb8a5e4c7d5b60
Author: Caideyipi <[email protected]>
AuthorDate: Tue Aug 4 17:48:20 2026 +0800

    Query: Optimize schema traversal for non-leading tag IN predicates (#18369)
---
 .../schemaregion/mtree/traverser/Traverser.java    |   6 +
 .../SchemaRegionTableDevicePerformanceTest.java    | 335 ++++++++++++++++++
 .../metadata/fetcher/SchemaPredicateUtilTest.java  | 125 +++++++
 .../iotdb/commons/path/ExtendedPartialPath.java    |  34 +-
 .../apache/iotdb/commons/path/fa/IPatternFA.java   |   8 +
 .../iotdb/commons/path/fa/nfa/SimpleNFA.java       | 102 +++++-
 .../schema/filter/impl/DeviceFilterUtil.java       | 261 +++++++++++++-
 .../commons/schema/tree/AbstractTreeVisitor.java   |  79 ++++-
 .../schema/filter/impl/DeviceFilterUtilTest.java   | 385 +++++++++++++++++++++
 9 files changed, 1314 insertions(+), 21 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/Traverser.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/Traverser.java
index 99486fa5475..b9968a97c76 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/Traverser.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/Traverser.java
@@ -253,6 +253,12 @@ public abstract class Traverser<R, N extends IMNode<N>> 
extends AbstractTreeVisi
     };
   }
 
+  @Override
+  protected int getChildrenSize(final N parent) {
+    // Only the memory MTree exposes the complete child key set without an 
extra traversal.
+    return parent instanceof IMemMNode ? parent.getChildren().size() : 
Integer.MAX_VALUE;
+  }
+
   @Override
   protected Iterator<N> getChildrenIterator(N parent) throws MetadataException 
{
     if (parent.isAboveDatabase()) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionTableDevicePerformanceTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionTableDevicePerformanceTest.java
new file mode 100644
index 00000000000..c3ee501426c
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionTableDevicePerformanceTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.metadata.schemaRegion;
+
+import org.apache.iotdb.commons.path.ExtendedPartialPath;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.commons.schema.filter.SchemaFilter;
+import org.apache.iotdb.commons.schema.filter.impl.DeviceFilterUtil;
+import org.apache.iotdb.commons.schema.filter.impl.singlechild.TagFilter;
+import org.apache.iotdb.commons.schema.filter.impl.values.PreciseFilter;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.CreateOrUpdateTableDeviceNode;
+import org.apache.iotdb.db.schemaengine.schemaregion.ISchemaRegion;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.read.resp.info.IDeviceSchemaInfo;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.read.resp.reader.ISchemaReader;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Measurement;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Summary;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import static org.apache.iotdb.commons.conf.IoTDBConstant.PATH_ROOT;
+
+public class SchemaRegionTableDevicePerformanceTest extends 
AbstractSchemaRegionTest {
+
+  private static final String ENABLED_PROPERTY = 
"iotdb.schema.non.leading.in.perf.enabled";
+  private static final String METERS_PROPERTY = 
"iotdb.schema.non.leading.in.perf.meters";
+  private static final String CHILDREN_PROPERTY =
+      "iotdb.schema.non.leading.in.perf.children.per.meter";
+  private static final String VALUES_PROPERTY = 
"iotdb.schema.non.leading.in.perf.values";
+  private static final String HIT_VALUES_PROPERTY = 
"iotdb.schema.non.leading.in.perf.hit.values";
+  private static final String BATCH_SIZE_PROPERTY = 
"iotdb.schema.non.leading.in.perf.batch.size";
+  private static final String WARMUPS_PROPERTY = 
"iotdb.schema.non.leading.in.perf.warmups";
+  private static final String ITERATIONS_PROPERTY = 
"iotdb.schema.non.leading.in.perf.iterations";
+  private static final String ROUNDS_PROPERTY = 
"iotdb.schema.non.leading.in.perf.rounds";
+
+  private static volatile long benchmarkBlackhole;
+
+  public SchemaRegionTableDevicePerformanceTest(final SchemaRegionTestParams 
testParams) {
+    super(testParams);
+  }
+
+  @Test
+  public void benchmarkNonLeadingTagInTraversal() throws Exception {
+    Assume.assumeTrue(
+        String.format(
+            "Manual performance UT. Enable with -D%s=true; tune the other 
iotdb.schema.non.leading.in.perf.* properties as needed.",
+            ENABLED_PROPERTY),
+        Boolean.getBoolean(ENABLED_PROPERTY));
+    Assume.assumeTrue(
+        "The table-device schema reader is available only in MemoryMode.",
+        testParams.getTestModeName().equals("MemoryMode"));
+    Assume.assumeTrue(
+        "Current-thread CPU time and allocation metrics are required.",
+        ManualPerformanceTestUtils.enableThreadMetrics());
+
+    final int meterCount = Integer.getInteger(METERS_PROPERTY, 20_000);
+    final int childrenPerMeter = Integer.getInteger(CHILDREN_PROPERTY, 8);
+    final List<Integer> valueCounts = getPositiveIntValues(VALUES_PROPERTY, 
"6");
+    final int hitValueCount = Integer.getInteger(HIT_VALUES_PROPERTY, 0);
+    final int batchSize = Integer.getInteger(BATCH_SIZE_PROPERTY, 1_000);
+    final int warmups = Integer.getInteger(WARMUPS_PROPERTY, 2);
+    final int iterations = Integer.getInteger(ITERATIONS_PROPERTY, 1);
+    final int rounds = Integer.getInteger(ROUNDS_PROPERTY, 5);
+    Assert.assertTrue(meterCount > 0);
+    Assert.assertTrue(childrenPerMeter > 0);
+    Assert.assertTrue(hitValueCount >= 0);
+    Assert.assertTrue(hitValueCount <= childrenPerMeter);
+    Assert.assertTrue(batchSize > 0);
+    Assert.assertTrue(warmups >= 0);
+    Assert.assertTrue(iterations > 0);
+    Assert.assertTrue(rounds > 0);
+
+    final ISchemaRegion schemaRegion = getSchemaRegion("db", 0);
+    final String tableName = "non_leading_in_perf";
+    createBenchmarkDevices(schemaRegion, tableName, meterCount, 
childrenPerMeter, batchSize);
+
+    for (final int valueCount : valueCounts) {
+      Assert.assertTrue(hitValueCount <= valueCount);
+      runBenchmarkScenario(
+          schemaRegion,
+          tableName,
+          meterCount,
+          childrenPerMeter,
+          valueCount,
+          hitValueCount,
+          warmups,
+          iterations,
+          rounds);
+    }
+  }
+
+  private static void runBenchmarkScenario(
+      final ISchemaRegion schemaRegion,
+      final String tableName,
+      final int meterCount,
+      final int childrenPerMeter,
+      final int valueCount,
+      final int hitValueCount,
+      final int warmups,
+      final int iterations,
+      final int rounds) {
+    final List<String> cardValues = new ArrayList<>(valueCount);
+    for (int i = 0; i < valueCount; ++i) {
+      cardValues.add(i < hitValueCount ? "card_" + i : "missing_card_" + i);
+    }
+
+    final List<PartialPath> legacyPatterns =
+        createLegacyExpandedPatterns(schemaRegion, tableName, cardValues);
+    final List<PartialPath> optimizedPatterns =
+        createOptimizedPatterns(schemaRegion, tableName, cardValues);
+    Assert.assertEquals(1, optimizedPatterns.size());
+
+    final long legacyMatches = scanPatterns(schemaRegion, legacyPatterns);
+    final long optimizedMatches = scanPatterns(schemaRegion, 
optimizedPatterns);
+    Assert.assertEquals(legacyMatches, optimizedMatches);
+
+    for (int i = 0; i < warmups; ++i) {
+      if ((i & 1) == 0) {
+        benchmarkBlackhole = scanPatterns(schemaRegion, legacyPatterns);
+        benchmarkBlackhole = scanPatterns(schemaRegion, optimizedPatterns);
+      } else {
+        benchmarkBlackhole = scanPatterns(schemaRegion, optimizedPatterns);
+        benchmarkBlackhole = scanPatterns(schemaRegion, legacyPatterns);
+      }
+    }
+
+    final Measurement[] legacyMeasurements = new Measurement[rounds];
+    final Measurement[] optimizedMeasurements = new Measurement[rounds];
+    for (int i = 0; i < rounds; ++i) {
+      if ((i & 1) == 0) {
+        legacyMeasurements[i] =
+            ManualPerformanceTestUtils.measure(
+                iterations, () -> benchmarkBlackhole = 
scanPatterns(schemaRegion, legacyPatterns));
+        optimizedMeasurements[i] =
+            ManualPerformanceTestUtils.measure(
+                iterations,
+                () -> benchmarkBlackhole = scanPatterns(schemaRegion, 
optimizedPatterns));
+      } else {
+        optimizedMeasurements[i] =
+            ManualPerformanceTestUtils.measure(
+                iterations,
+                () -> benchmarkBlackhole = scanPatterns(schemaRegion, 
optimizedPatterns));
+        legacyMeasurements[i] =
+            ManualPerformanceTestUtils.measure(
+                iterations, () -> benchmarkBlackhole = 
scanPatterns(schemaRegion, legacyPatterns));
+      }
+    }
+
+    printTraversalBenchmark(
+        meterCount,
+        childrenPerMeter,
+        valueCount,
+        hitValueCount,
+        legacyMatches,
+        warmups,
+        iterations,
+        rounds,
+        ManualPerformanceTestUtils.summarize(legacyMeasurements, iterations),
+        ManualPerformanceTestUtils.summarize(optimizedMeasurements, 
iterations));
+  }
+
+  private static List<Integer> getPositiveIntValues(
+      final String propertyName, final String defaultValue) {
+    final String[] rawValues = System.getProperty(propertyName, 
defaultValue).split(",");
+    final List<Integer> values = new ArrayList<>(rawValues.length);
+    for (final String rawValue : rawValues) {
+      final int value = Integer.parseInt(rawValue.trim());
+      Assert.assertTrue(value > 0);
+      values.add(value);
+    }
+    return values;
+  }
+
+  private static void createBenchmarkDevices(
+      final ISchemaRegion schemaRegion,
+      final String tableName,
+      final int meterCount,
+      final int childrenPerMeter,
+      final int batchSize)
+      throws Exception {
+    final List<Object[]> deviceIds = new ArrayList<>(batchSize);
+    for (int meter = 0; meter < meterCount; ++meter) {
+      for (int child = 0; child < childrenPerMeter; ++child) {
+        deviceIds.add(new Object[] {"meter_" + meter, "card_" + child});
+        if (deviceIds.size() == batchSize) {
+          createBenchmarkDeviceBatch(schemaRegion, tableName, deviceIds);
+          deviceIds.clear();
+        }
+      }
+    }
+    if (!deviceIds.isEmpty()) {
+      createBenchmarkDeviceBatch(schemaRegion, tableName, deviceIds);
+    }
+  }
+
+  private static void createBenchmarkDeviceBatch(
+      final ISchemaRegion schemaRegion, final String tableName, final 
List<Object[]> deviceIds)
+      throws Exception {
+    schemaRegion.createOrUpdateTableDevice(
+        new CreateOrUpdateTableDeviceNode(
+            new PlanNodeId("non-leading-in-performance"),
+            null,
+            tableName,
+            new ArrayList<>(deviceIds),
+            Collections.emptyList(),
+            Collections.nCopies(deviceIds.size(), new Object[0])));
+  }
+
+  private static List<PartialPath> createLegacyExpandedPatterns(
+      final ISchemaRegion schemaRegion, final String tableName, final 
List<String> cardValues) {
+    final List<PartialPath> patterns = new ArrayList<>(cardValues.size());
+    for (final String cardValue : cardValues) {
+      patterns.add(
+          new ExtendedPartialPath(
+              new String[] {
+                PATH_ROOT, schemaRegion.getDatabaseFullPath(), tableName, "*", 
cardValue
+              },
+              false));
+    }
+    return patterns;
+  }
+
+  private static List<PartialPath> createOptimizedPatterns(
+      final ISchemaRegion schemaRegion, final String tableName, final 
List<String> cardValues) {
+    final List<List<SchemaFilter>> filterBranches = new 
ArrayList<>(cardValues.size());
+    for (final String cardValue : cardValues) {
+      filterBranches.add(Collections.singletonList(new TagFilter(new 
PreciseFilter(cardValue), 1)));
+    }
+    return DeviceFilterUtil.convertToDevicePattern(
+        new String[] {PATH_ROOT, schemaRegion.getDatabaseFullPath(), 
tableName},
+        2,
+        filterBranches,
+        false);
+  }
+
+  private static long scanPatterns(
+      final ISchemaRegion schemaRegion, final List<PartialPath> patterns) {
+    long count = 0;
+    for (final PartialPath pattern : patterns) {
+      try (final ISchemaReader<IDeviceSchemaInfo> reader =
+          schemaRegion.getTableDeviceReader(pattern)) {
+        while (reader.hasNext()) {
+          reader.next();
+          ++count;
+        }
+      } catch (final Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return count;
+  }
+
+  private static void printTraversalBenchmark(
+      final int meterCount,
+      final int childrenPerMeter,
+      final int valueCount,
+      final int hitValueCount,
+      final long matchCount,
+      final int warmups,
+      final int iterations,
+      final int rounds,
+      final Summary legacy,
+      final Summary optimized) {
+    System.out.printf(
+        Locale.ROOT,
+        "Non-leading TAG IN traversal benchmark: meters=%d, children/meter=%d, 
devices=%d, IN values=%d, hit values=%d, matches=%d, warmups=%d, 
iterations/round=%d, rounds=%d%n",
+        meterCount,
+        childrenPerMeter,
+        (long) meterCount * childrenPerMeter,
+        valueCount,
+        hitValueCount,
+        matchCount,
+        warmups,
+        iterations,
+        rounds);
+    System.out.printf(
+        Locale.ROOT,
+        "  optimized strategy: %s (IN values=%d, child keys=%d)%n",
+        valueCount < childrenPerMeter ? "precise IN lookups" : "child-key 
iteration",
+        valueCount,
+        childrenPerMeter);
+    printTraversalSummary("legacy-expanded", legacy);
+    printTraversalSummary("optimized", optimized);
+    System.out.printf(
+        Locale.ROOT,
+        "  change: CPU speedup=%.2fx, allocation reduction=%.1f%%, peak-heap 
reduction=%.1f%%%n",
+        ratio(legacy.getCpuNanosPerOperation(), 
optimized.getCpuNanosPerOperation()),
+        reduction(
+            legacy.getAllocatedBytesPerOperation(), 
optimized.getAllocatedBytesPerOperation()),
+        reduction(legacy.getPeakHeapDeltaBytes(), 
optimized.getPeakHeapDeltaBytes()));
+  }
+
+  private static void printTraversalSummary(final String label, final Summary 
summary) {
+    System.out.printf(
+        Locale.ROOT,
+        "  %-15s CPU=%.3f ms/op, allocated=%.1f bytes/op, peak heap delta=%.3f 
MiB%n",
+        label,
+        summary.getCpuNanosPerOperation() / 1_000_000.0,
+        summary.getAllocatedBytesPerOperation(),
+        summary.getPeakHeapDeltaBytes() / 1024.0 / 1024.0);
+  }
+
+  private static double ratio(final double baseline, final double optimized) {
+    return optimized == 0 ? Double.POSITIVE_INFINITY : baseline / optimized;
+  }
+
+  private static double reduction(final double baseline, final double 
optimized) {
+    return baseline == 0 ? 0 : (baseline - optimized) * 100.0 / baseline;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/SchemaPredicateUtilTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/SchemaPredicateUtilTest.java
new file mode 100644
index 00000000000..10d21bfd2c8
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/SchemaPredicateUtilTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.path.PartialPath;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ComparisonExpression;
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.InListExpression;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.InPredicate;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.LogicalExpression;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.StringLiteral;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.SymbolReference;
+import org.apache.iotdb.commons.schema.filter.SchemaFilter;
+import org.apache.iotdb.commons.schema.filter.impl.DeviceFilterUtil;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.column.TagColumnSchema;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SchemaPredicateUtilTest {
+
+  private static final String[] PREFIX = new String[] {"root", "db", "table"};
+
+  @Test
+  public void testCompactExpandedNonLeadingInAndOr() {
+    final TsTable table = createTable();
+
+    final AtomicBoolean inMayContainDuplicate = new AtomicBoolean(false);
+    final List<PartialPath> inPatterns =
+        convertToDevicePatterns(
+            table,
+            new InPredicate(
+                new SymbolReference("cardno"),
+                new InListExpression(
+                    Arrays.asList(new StringLiteral("card1"), new 
StringLiteral("card2")))),
+            inMayContainDuplicate);
+    Assert.assertFalse(inMayContainDuplicate.get());
+    Assert.assertEquals(1, inPatterns.size());
+
+    final AtomicBoolean orMayContainDuplicate = new AtomicBoolean(false);
+    final List<PartialPath> orPatterns =
+        convertToDevicePatterns(
+            table,
+            new LogicalExpression(
+                LogicalExpression.Operator.OR,
+                Arrays.asList(equal("cardno", "card1"), equal("cardno", 
"card2"))),
+            orMayContainDuplicate);
+    Assert.assertTrue(orMayContainDuplicate.get());
+    Assert.assertEquals(1, orPatterns.size());
+  }
+
+  @Test
+  public void testKeepExpandedLeadingIn() {
+    final TsTable table = createTable();
+    final List<PartialPath> patterns =
+        convertToDevicePatterns(
+            table,
+            new InPredicate(
+                new SymbolReference("meterinfoid"),
+                new InListExpression(
+                    Arrays.asList(new StringLiteral("meter1"), new 
StringLiteral("meter2")))),
+            new AtomicBoolean(false));
+
+    Assert.assertEquals(2, patterns.size());
+    Assert.assertEquals("root.db.table.meter1.*", 
patterns.get(0).getFullPath());
+    Assert.assertEquals("root.db.table.meter2.*", 
patterns.get(1).getFullPath());
+  }
+
+  private static List<PartialPath> convertToDevicePatterns(
+      final TsTable table,
+      final Expression expression,
+      final AtomicBoolean mayContainDuplicateDevice) {
+    final List<Map<Integer, List<SchemaFilter>>> filterMaps =
+        SchemaPredicateUtil.convertTagPredicateToOrConcatList(
+            Collections.singletonList(expression), table, 
mayContainDuplicateDevice);
+    Assert.assertEquals(2, filterMaps.size());
+
+    final List<List<SchemaFilter>> filterBranches = new 
ArrayList<>(filterMaps.size());
+    for (final Map<Integer, List<SchemaFilter>> filterMap : filterMaps) {
+      final List<SchemaFilter> branch = new ArrayList<>();
+      filterMap.values().forEach(branch::addAll);
+      filterBranches.add(branch);
+    }
+    return DeviceFilterUtil.convertToDevicePattern(
+        PREFIX, table.getTagNum(), filterBranches, false);
+  }
+
+  private static ComparisonExpression equal(final String column, final String 
value) {
+    return new ComparisonExpression(
+        ComparisonExpression.Operator.EQUAL, new SymbolReference(column), new 
StringLiteral(value));
+  }
+
+  private static TsTable createTable() {
+    final TsTable table = new TsTable("table");
+    table.addColumnSchema(new TagColumnSchema("meterinfoid", 
TSDataType.STRING));
+    table.addColumnSchema(new TagColumnSchema("cardno", TSDataType.STRING));
+    return table;
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/ExtendedPartialPath.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/ExtendedPartialPath.java
index 7976f44d8ef..0cf68f6918c 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/ExtendedPartialPath.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/ExtendedPartialPath.java
@@ -21,12 +21,15 @@ package org.apache.iotdb.commons.path;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 
 public class ExtendedPartialPath extends PartialPath {
   private final Map<Integer, List<Function<String, Boolean>>> matchFunctions = 
new HashMap<>();
+  private final Map<Integer, Set<String>> multiExactMatchNodes = new 
HashMap<>();
   private final boolean isRestrict;
 
   public ExtendedPartialPath(final String[] nodes, final boolean isRestrict) {
@@ -39,6 +42,10 @@ public class ExtendedPartialPath extends PartialPath {
   }
 
   public boolean match(final int index, final String value) {
+    if (multiExactMatchNodes.containsKey(index)
+        && !multiExactMatchNodes.get(index).contains(value)) {
+      return false;
+    }
     if (!matchFunctions.containsKey(index)) {
       return true;
     }
@@ -49,8 +56,31 @@ public class ExtendedPartialPath extends PartialPath {
     matchFunctions.computeIfAbsent(index, k -> new 
ArrayList<>()).add(matchFunction);
   }
 
+  public void addMultiExactMatch(final int index, final Set<String> values) {
+    multiExactMatchNodes.compute(
+        index,
+        (key, existingValues) -> {
+          if (existingValues == null) {
+            final Set<String> copiedValues = new HashSet<>(values);
+            copiedValues.remove(null);
+            return copiedValues;
+          }
+          existingValues.retainAll(values);
+          existingValues.remove(null);
+          return existingValues;
+        });
+  }
+
+  public boolean hasMultiExactMatch(final int index) {
+    return multiExactMatchNodes.containsKey(index);
+  }
+
+  public Set<String> getMultiExactMatch(final int index) {
+    return multiExactMatchNodes.get(index);
+  }
+
   public boolean isNormalPath() {
-    return matchFunctions.isEmpty();
+    return matchFunctions.isEmpty() && multiExactMatchNodes.isEmpty();
   }
 
   @Override
@@ -60,6 +90,8 @@ public class ExtendedPartialPath extends PartialPath {
         + getFullPath()
         + ", matchFunctions="
         + matchFunctions
+        + ", multiExactMatchNodes="
+        + multiExactMatchNodes
         + ", isRestrict="
         + isRestrict
         + '}';
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/IPatternFA.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/IPatternFA.java
index 30249cdf694..ffacb6106b2 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/IPatternFA.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/IPatternFA.java
@@ -58,6 +58,14 @@ public interface IPatternFA {
    */
   int getFuzzyMatchTransitionSize(IFAState state);
 
+  /**
+   * @param state the source state of the transitions
+   * @return whether the precise transitions from this state represent a 
multi-exact match
+   */
+  default boolean hasMultiExactMatchTransitions(IFAState state) {
+    return false;
+  }
+
   /**
    * @param sourceState source state
    * @param transition transition that the source state has
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/nfa/SimpleNFA.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/nfa/SimpleNFA.java
index 2a0304394ca..e7390801461 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/nfa/SimpleNFA.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/fa/nfa/SimpleNFA.java
@@ -27,10 +27,12 @@ import org.apache.iotdb.commons.path.fa.IFATransition;
 import org.apache.iotdb.commons.path.fa.IPatternFA;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 
@@ -115,8 +117,16 @@ public class SimpleNFA implements IPatternFA {
     return getNextNode((SinglePathPatternNode) 
state).getPreNodeFuzzyMatchTransitionSize();
   }
 
+  @Override
+  public boolean hasMultiExactMatchTransitions(final IFAState state) {
+    return getNextNode((SinglePathPatternNode) state) instanceof 
MultiExactMatchNode;
+  }
+
   @Override
   public IFAState getNextState(IFAState sourceState, IFATransition transition) 
{
+    if (transition instanceof MultiExactTransition) {
+      return ((MultiExactTransition) transition).targetNode;
+    }
     return (SinglePathPatternNode) transition;
   }
 
@@ -154,13 +164,21 @@ public class SimpleNFA implements IPatternFA {
       } else if (rawNodes[nextIndex].equals(MULTI_LEVEL_PATH_WILDCARD)) {
         patternNodes[nextIndex] = new MultiLevelWildcardMatchNode(nextIndex);
       } else if (rawNodes[nextIndex].equals(ONE_LEVEL_PATH_WILDCARD)) {
+        final ExtendedPartialPath extendedPath =
+            pathPattern instanceof ExtendedPartialPath ? (ExtendedPartialPath) 
pathPattern : null;
         patternNodes[nextIndex] =
-            new OneLevelWildcardMatchNode(
-                nextIndex,
-                currentNode.getTracebackNode(),
-                pathPattern instanceof ExtendedPartialPath
-                    ? event -> ((ExtendedPartialPath) 
pathPattern).match(nextIndex, event)
-                    : event -> true);
+            extendedPath != null && extendedPath.hasMultiExactMatch(nextIndex)
+                ? new MultiExactMatchNode(
+                    nextIndex,
+                    currentNode.getTracebackNode(),
+                    extendedPath.getMultiExactMatch(nextIndex),
+                    event -> extendedPath.match(nextIndex, event))
+                : new OneLevelWildcardMatchNode(
+                    nextIndex,
+                    currentNode.getTracebackNode(),
+                    extendedPath != null
+                        ? event -> extendedPath.match(nextIndex, event)
+                        : event -> true);
       } else if (PathPatternUtil.hasWildcard(rawNodes[nextIndex])) {
         patternNodes[nextIndex] = new RegexMatchNode(nextIndex, 
currentNode.getTracebackNode());
       } else {
@@ -422,6 +440,78 @@ public class SimpleNFA implements IPatternFA {
     }
   }
 
+  /** The patternNode of a group of specified names. */
+  private class MultiExactMatchNode extends SinglePathPatternNode {
+
+    private final Map<String, IFATransition> preciseTransitions = new 
HashMap<>();
+
+    private MultiExactMatchNode(
+        final int patternIndex,
+        final SinglePathPatternNode tracebackNode,
+        final Set<String> values,
+        final Function<String, Boolean> matchFunction) {
+      super(patternIndex, tracebackNode);
+      for (final String value : values) {
+        if (matchFunction.apply(value)) {
+          preciseTransitions.put(value, new MultiExactTransition(value, this));
+        }
+      }
+    }
+
+    @Override
+    public boolean isMatch(final String event) {
+      return preciseTransitions.containsKey(event);
+    }
+
+    @Override
+    protected Map<String, IFATransition> getPreNodePreciseMatchTransition() {
+      return preciseTransitions;
+    }
+
+    @Override
+    protected Iterator<IFATransition> 
getPreNodePreciseMatchTransitionIterator() {
+      return preciseTransitions.values().iterator();
+    }
+
+    @Override
+    protected Iterator<IFATransition> getPreNodeFuzzyMatchTransitionIterator() 
{
+      return tracebackNode == null
+          ? Collections.emptyIterator()
+          : new SingletonIterator<>(tracebackNode);
+    }
+
+    @Override
+    protected int getPreNodeFuzzyMatchTransitionSize() {
+      return tracebackNode == null ? 0 : 1;
+    }
+  }
+
+  private class MultiExactTransition implements IFATransition {
+
+    private final String acceptEvent;
+    private final MultiExactMatchNode targetNode;
+
+    private MultiExactTransition(final String acceptEvent, final 
MultiExactMatchNode targetNode) {
+      this.acceptEvent = acceptEvent;
+      this.targetNode = targetNode;
+    }
+
+    @Override
+    public String getAcceptEvent() {
+      return acceptEvent;
+    }
+
+    @Override
+    public boolean isMatch(final String event) {
+      return Objects.equals(acceptEvent, event);
+    }
+
+    @Override
+    public int getIndex() {
+      return targetNode.getIndex();
+    }
+  }
+
   /** The patternNode of the rawNode contains *, like d*. */
   private class RegexMatchNode extends SinglePathPatternNode {
 
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtil.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtil.java
index 1e49eb915d5..9786f2161cd 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtil.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtil.java
@@ -25,11 +25,19 @@ import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.schema.filter.SchemaFilter;
 import org.apache.iotdb.commons.schema.filter.SchemaFilterType;
 import org.apache.iotdb.commons.schema.filter.impl.singlechild.TagFilter;
+import org.apache.iotdb.commons.schema.filter.impl.values.InFilter;
 import org.apache.iotdb.commons.schema.filter.impl.values.PreciseFilter;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
 
 import static 
org.apache.iotdb.commons.conf.IoTDBConstant.ONE_LEVEL_PATH_WILDCARD;
 
@@ -49,7 +57,8 @@ public class DeviceFilterUtil {
       final boolean isRestrict) {
     final List<PartialPath> pathList = new ArrayList<>();
     final int length = tagColumnNum + prefix.length;
-    for (final List<SchemaFilter> tagFilterList : tagDeterminedFilterList) {
+    for (final List<SchemaFilter> tagFilterList :
+        compactNonLeadingPreciseFilters(tagColumnNum, 
tagDeterminedFilterList)) {
       final String[] nodes = new String[length];
       Arrays.fill(nodes, ONE_LEVEL_PATH_WILDCARD);
       System.arraycopy(prefix, 0, nodes, 0, prefix.length);
@@ -62,6 +71,8 @@ public class DeviceFilterUtil {
             // If there is a precise filter, other filters on the same id are 
processed and thus
             // not exist here
             nodes[index] = ((PreciseFilter) childFilter).getValue();
+          } else if 
(childFilter.getSchemaFilterType().equals(SchemaFilterType.IN)) {
+            partialPath.addMultiExactMatch(index, ((InFilter) 
childFilter).getValues());
           } else {
             partialPath.addMatchFunction(
                 index,
@@ -78,4 +89,252 @@ public class DeviceFilterUtil {
     }
     return pathList;
   }
+
+  /**
+   * IN predicates and OR-connected equal predicates are expanded into precise 
filter branches so
+   * that complete device IDs can use the device cache. If a precise filter is 
behind an unfixed tag
+   * level, however, every expanded branch traverses the same wildcard 
subtree. Compact branches
+   * that differ only at such a tag into one IN filter to avoid repeated 
schema traversals.
+   */
+  private static List<List<SchemaFilter>> compactNonLeadingPreciseFilters(
+      final int tagColumnNum, final List<List<SchemaFilter>> 
tagDeterminedFilterList) {
+    if (tagColumnNum <= 1 || tagDeterminedFilterList.size() <= 1) {
+      return tagDeterminedFilterList;
+    }
+    if (areAllBranchesFullyPrecise(tagColumnNum, tagDeterminedFilterList)) {
+      return tagDeterminedFilterList;
+    }
+
+    List<FilterBranch> filterBranches = new 
ArrayList<>(tagDeterminedFilterList.size());
+    final Map<FilterSequenceKey, Integer> suffixSequenceIds = new HashMap<>();
+    for (final List<SchemaFilter> tagFilterList : tagDeterminedFilterList) {
+      final Map<Integer, List<SchemaFilter>> filterMap = new TreeMap<>();
+      for (final SchemaFilter schemaFilter : tagFilterList) {
+        if (!schemaFilter.getSchemaFilterType().equals(SchemaFilterType.TAG)) {
+          throw new IllegalStateException(
+              SchemaMessages.INPUT_SINGLE_FILTER_MUST_BE_DEVICE_ID_FILTER);
+        }
+        final int tagIndex = ((TagFilter) schemaFilter).getIndex();
+        filterMap.computeIfAbsent(tagIndex, key -> new 
ArrayList<>()).add(schemaFilter);
+      }
+      filterBranches.add(new FilterBranch(filterMap, tagColumnNum, 
suffixSequenceIds));
+    }
+
+    final Map<FilterSequenceKey, Integer> prefixSequenceIds = new HashMap<>();
+    for (final FilterBranch filterBranch : filterBranches) {
+      filterBranch.prefixSequenceId =
+          getSequenceId(prefixSequenceIds, 0, filterBranch.filterMap.get(0));
+    }
+    // A branch group is identified by everything before and after the current 
tag. Canonical
+    // sequence IDs make both the eligibility check and the group-key 
construction O(1) per branch
+    // at each tag, while prefix IDs are updated after earlier precise filters 
are compacted to IN.
+    for (int tagIndex = 1; tagIndex < tagColumnNum; tagIndex++) {
+      filterBranches = compactPreciseFiltersAtIndex(filterBranches, tagIndex);
+      for (final FilterBranch filterBranch : filterBranches) {
+        final List<SchemaFilter> currentFilters = 
filterBranch.filterMap.get(tagIndex);
+        filterBranch.prefixSequenceId =
+            getSequenceId(prefixSequenceIds, filterBranch.prefixSequenceId, 
currentFilters);
+        filterBranch.hasNonPrecisePrefix |= 
!isSinglePreciseTagFilter(currentFilters);
+      }
+    }
+
+    final List<List<SchemaFilter>> result = new 
ArrayList<>(filterBranches.size());
+    for (final FilterBranch filterBranch : filterBranches) {
+      final List<SchemaFilter> filterList = new ArrayList<>();
+      filterBranch.filterMap.values().forEach(filterList::addAll);
+      result.add(filterList);
+    }
+    return result;
+  }
+
+  private static boolean areAllBranchesFullyPrecise(
+      final int tagColumnNum, final List<List<SchemaFilter>> 
tagDeterminedFilterList) {
+    final int[] visitedTagIndexes = new int[tagColumnNum];
+    int branchId = 1;
+    for (final List<SchemaFilter> tagFilterList : tagDeterminedFilterList) {
+      if (tagFilterList.size() != tagColumnNum) {
+        return false;
+      }
+      for (final SchemaFilter schemaFilter : tagFilterList) {
+        if (!isPreciseTagFilter(schemaFilter)) {
+          return false;
+        }
+        final int tagIndex = ((TagFilter) schemaFilter).getIndex();
+        if (tagIndex < 0 || tagIndex >= tagColumnNum || 
visitedTagIndexes[tagIndex] == branchId) {
+          return false;
+        }
+        visitedTagIndexes[tagIndex] = branchId;
+      }
+      branchId++;
+    }
+    return true;
+  }
+
+  private static List<FilterBranch> compactPreciseFiltersAtIndex(
+      final List<FilterBranch> filterBranches, final int tagIndex) {
+    final Map<FilterBranchKey, List<FilterBranch>> filterGroupMap = new 
HashMap<>();
+    for (final FilterBranch filterBranch : filterBranches) {
+      if (!filterBranch.hasNonPrecisePrefix
+          || !isSinglePreciseTagFilter(filterBranch.filterMap.get(tagIndex))) {
+        continue;
+      }
+      final FilterBranchKey filterBranchKey =
+          new FilterBranchKey(
+              filterBranch.prefixSequenceId, 
filterBranch.suffixSequenceIds[tagIndex]);
+      filterGroupMap.computeIfAbsent(filterBranchKey, key -> new 
ArrayList<>()).add(filterBranch);
+    }
+
+    if (filterGroupMap.isEmpty()) {
+      return filterBranches;
+    }
+
+    int removedBranchCount = 0;
+    for (final List<FilterBranch> filterGroup : filterGroupMap.values()) {
+      if (filterGroup.size() <= 1) {
+        continue;
+      }
+
+      final Set<String> preciseValues = new HashSet<>();
+      for (final FilterBranch filterBranch : filterGroup) {
+        preciseValues.add(
+            ((PreciseFilter) ((TagFilter) 
filterBranch.filterMap.get(tagIndex).get(0)).getChild())
+                .getValue());
+      }
+      if (preciseValues.contains(null)) {
+        continue;
+      }
+
+      if (preciseValues.size() > 1) {
+        filterGroup
+            .get(0)
+            .filterMap
+            .put(
+                tagIndex,
+                Collections.singletonList(new TagFilter(new 
InFilter(preciseValues), tagIndex)));
+      }
+      for (int i = 1; i < filterGroup.size(); i++) {
+        filterGroup.get(i).removed = true;
+        removedBranchCount++;
+      }
+    }
+
+    if (removedBranchCount == 0) {
+      return filterBranches;
+    }
+
+    final List<FilterBranch> result = new ArrayList<>(filterBranches.size() - 
removedBranchCount);
+    for (final FilterBranch filterBranch : filterBranches) {
+      if (!filterBranch.removed) {
+        result.add(filterBranch);
+      }
+    }
+    return result;
+  }
+
+  private static int getSequenceId(
+      final Map<FilterSequenceKey, Integer> sequenceIds,
+      final int previousSequenceId,
+      final List<SchemaFilter> filters) {
+    final FilterSequenceKey key = new FilterSequenceKey(previousSequenceId, 
filters);
+    final Integer existingSequenceId = sequenceIds.get(key);
+    if (existingSequenceId != null) {
+      return existingSequenceId;
+    }
+    final int newSequenceId = sequenceIds.size() + 1;
+    sequenceIds.put(key, newSequenceId);
+    return newSequenceId;
+  }
+
+  private static boolean isSinglePreciseTagFilter(final List<SchemaFilter> 
filters) {
+    return filters != null && filters.size() == 1 && 
isPreciseTagFilter(filters.get(0));
+  }
+
+  private static boolean isPreciseTagFilter(final SchemaFilter schemaFilter) {
+    return schemaFilter.getSchemaFilterType().equals(SchemaFilterType.TAG)
+        && ((TagFilter) schemaFilter)
+            .getChild()
+            .getSchemaFilterType()
+            .equals(SchemaFilterType.PRECISE);
+  }
+
+  private static final class FilterBranch {
+
+    private final Map<Integer, List<SchemaFilter>> filterMap;
+    private final int[] suffixSequenceIds;
+    private int prefixSequenceId;
+    private boolean hasNonPrecisePrefix;
+    private boolean removed;
+
+    private FilterBranch(
+        final Map<Integer, List<SchemaFilter>> filterMap,
+        final int tagColumnNum,
+        final Map<FilterSequenceKey, Integer> suffixSequenceIdMap) {
+      this.filterMap = filterMap;
+      this.suffixSequenceIds = new int[tagColumnNum];
+      this.hasNonPrecisePrefix = !isSinglePreciseTagFilter(filterMap.get(0));
+
+      int suffixSequenceId = 0;
+      for (int tagIndex = tagColumnNum - 1; tagIndex >= 0; tagIndex--) {
+        suffixSequenceIds[tagIndex] = suffixSequenceId;
+        suffixSequenceId =
+            getSequenceId(suffixSequenceIdMap, suffixSequenceId, 
filterMap.get(tagIndex));
+      }
+    }
+  }
+
+  private static final class FilterSequenceKey {
+
+    private final int previousSequenceId;
+    private final List<SchemaFilter> filters;
+
+    private FilterSequenceKey(final int previousSequenceId, final 
List<SchemaFilter> filters) {
+      this.previousSequenceId = previousSequenceId;
+      this.filters = filters;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof FilterSequenceKey)) {
+        return false;
+      }
+      final FilterSequenceKey that = (FilterSequenceKey) o;
+      return previousSequenceId == that.previousSequenceId && 
Objects.equals(filters, that.filters);
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * previousSequenceId + Objects.hashCode(filters);
+    }
+  }
+
+  private static final class FilterBranchKey {
+
+    private final int prefixSequenceId;
+    private final int suffixSequenceId;
+
+    private FilterBranchKey(final int prefixSequenceId, final int 
suffixSequenceId) {
+      this.prefixSequenceId = prefixSequenceId;
+      this.suffixSequenceId = suffixSequenceId;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof FilterBranchKey)) {
+        return false;
+      }
+      final FilterBranchKey that = (FilterBranchKey) o;
+      return prefixSequenceId == that.prefixSequenceId && suffixSequenceId == 
that.suffixSequenceId;
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * prefixSequenceId + suffixSequenceId;
+    }
+  }
 }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/tree/AbstractTreeVisitor.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/tree/AbstractTreeVisitor.java
index 512270c379a..04f59402891 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/tree/AbstractTreeVisitor.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/tree/AbstractTreeVisitor.java
@@ -402,6 +402,14 @@ public abstract class AbstractTreeVisitor<N extends 
ITreeNode, R> implements Sch
   protected abstract Iterator<N> getChildrenIterator(N parent, 
Iterator<String> childrenName)
       throws Exception;
 
+  /**
+   * Get the number of child keys that can be iterated directly. Return {@link 
Integer#MAX_VALUE}
+   * when the number is unavailable, so precise transitions are preferred.
+   */
+  protected int getChildrenSize(final N parent) {
+    return Integer.MAX_VALUE;
+  }
+
   /**
    * Get current children iterator generated by {@link 
AbstractTreeVisitor#getChildrenIterator}
    *
@@ -551,40 +559,85 @@ public abstract class AbstractTreeVisitor<N extends 
ITreeNode, R> implements Sch
   // the child can be got directly with the precise value of transition, 
there's no traceback
   private class PreciseMatchChildrenIterator extends AbstractChildrenIterator {
     private final IFAState sourceState;
+    private final Map<String, IFATransition> preciseMatchTransitionMap;
     private final Iterator<IFATransition> transitionIterator;
+    private final boolean iterateChildren;
+    private Iterator<N> childrenIterator;
 
     private PreciseMatchChildrenIterator(N parent, IStateMatchInfo 
stateMatchInfo) {
       super(parent, stateMatchInfo.getScopeMatchedState());
       this.sourceState = stateMatchInfo.getOneMatchedState();
-      transitionIterator = 
patternFA.getPreciseMatchTransitionIterator(sourceState);
+      this.preciseMatchTransitionMap = 
patternFA.getPreciseMatchTransition(sourceState);
+      this.transitionIterator = preciseMatchTransitionMap.values().iterator();
+      // Candidate counts are a rough cost estimate. Iterate the smaller side 
so this remains
+      // adaptive without relying on unstable wall-clock thresholds. Prefer 
child iteration on a
+      // tie because it avoids a direct child lookup for every transition.
+      this.iterateChildren =
+          patternFA.hasMultiExactMatchTransitions(sourceState)
+              && getChildrenSize(parent) <= preciseMatchTransitionMap.size();
     }
 
     @Override
     protected void getNext() throws Exception {
+      if (iterateChildren) {
+        if (childrenIterator == null) {
+          childrenIterator = initChildrenIterator();
+        }
+        while (childrenIterator.hasNext()) {
+          final N child = childrenIterator.next();
+          IFATransition transition = 
preciseMatchTransitionMap.get(child.getName());
+          if (transition == null && child.getAlias() != null) {
+            transition = preciseMatchTransitionMap.get(child.getAlias());
+          }
+          if (transition == null) {
+            releaseNode(child);
+            continue;
+          }
+          if (trySaveResult(child, transition)) {
+            return;
+          }
+        }
+        return;
+      }
+
       IFATransition transition;
       while (transitionIterator.hasNext()) {
         transition = transitionIterator.next();
-        N child = getChild(parent, transition.getAcceptEvent());
+        final N child = getChild(parent, transition.getAcceptEvent());
         if (child == null) {
           continue;
         }
-        IFAState nextScopeState =
-            allScope ? null : getNextMatchedScopeState(currentScopeState, 
child);
-        if (!allScope && nextScopeState == null) {
-          releaseNode(child);
-          continue;
+        if (trySaveResult(child, transition)) {
+          return;
         }
-        saveResult(
-            child,
-            new StateSingleMatchInfo(
-                patternFA, patternFA.getNextState(sourceState, transition), 
nextScopeState));
-        return;
       }
     }
 
+    private boolean trySaveResult(final N child, final IFATransition 
transition) {
+      final IFAState nextScopeState =
+          allScope ? null : getNextMatchedScopeState(currentScopeState, child);
+      if (!allScope && nextScopeState == null) {
+        releaseNode(child);
+        return false;
+      }
+      saveResult(
+          child,
+          new StateSingleMatchInfo(
+              patternFA, patternFA.getNextState(sourceState, transition), 
nextScopeState));
+      return true;
+    }
+
     @Override
     public Iterator<N> getIterator() {
-      return null;
+      return childrenIterator;
+    }
+
+    @Override
+    protected void close() {
+      super.close();
+      if (childrenIterator != null) {
+        releaseNodeIterator(childrenIterator);
+      }
     }
   }
 
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtilTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtilTest.java
new file mode 100644
index 00000000000..b74d7f007fd
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/schema/filter/impl/DeviceFilterUtilTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.commons.schema.filter.impl;
+
+import org.apache.iotdb.commons.path.ExtendedPartialPath;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.path.fa.IFAState;
+import org.apache.iotdb.commons.path.fa.IFATransition;
+import org.apache.iotdb.commons.path.fa.IPatternFA;
+import org.apache.iotdb.commons.schema.filter.SchemaFilter;
+import org.apache.iotdb.commons.schema.filter.impl.singlechild.NotFilter;
+import org.apache.iotdb.commons.schema.filter.impl.singlechild.TagFilter;
+import org.apache.iotdb.commons.schema.filter.impl.values.InFilter;
+import org.apache.iotdb.commons.schema.filter.impl.values.PreciseFilter;
+import org.apache.iotdb.commons.schema.tree.AbstractTreeVisitor;
+import org.apache.iotdb.commons.schema.tree.ITreeNode;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DeviceFilterUtilTest {
+
+  private static final String[] PREFIX = new String[] {"root", "db", "table"};
+  private static final int TAG_COLUMN_NUM = 3;
+
+  @Test
+  public void testCompactExpandedInOrOnNonLeadingTag() {
+    final List<PartialPath> patterns =
+        convert(branch(precise(1, "card1")), branch(precise(1, "card2")));
+
+    Assert.assertEquals(1, patterns.size());
+    final ExtendedPartialPath pattern = (ExtendedPartialPath) patterns.get(0);
+    Assert.assertEquals("root.db.table.*.*.*", pattern.getFullPath());
+    Assert.assertTrue(pattern.match(PREFIX.length + 1, "card1"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 1, "card2"));
+    Assert.assertFalse(pattern.match(PREFIX.length + 1, "card3"));
+    assertPreciseTransitionsAfterFirstTag(pattern, "card1", "card2");
+  }
+
+  @Test
+  public void testKeepLeadingTagBranchesSeparated() {
+    final List<PartialPath> patterns =
+        convert(branch(precise(0, "meter1")), branch(precise(0, "meter2")));
+
+    Assert.assertEquals(2, patterns.size());
+    Assert.assertEquals("root.db.table.meter1.*.*", 
patterns.get(0).getFullPath());
+    Assert.assertEquals("root.db.table.meter2.*.*", 
patterns.get(1).getFullPath());
+  }
+
+  @Test
+  public void testKeepBranchesSeparatedWithCompletePrefix() {
+    final List<PartialPath> patterns =
+        convert(
+            branch(precise(0, "meter1"), precise(1, "card1")),
+            branch(precise(0, "meter1"), precise(1, "card2")));
+
+    Assert.assertEquals(2, patterns.size());
+    Assert.assertEquals("root.db.table.meter1.card1.*", 
patterns.get(0).getFullPath());
+    Assert.assertEquals("root.db.table.meter1.card2.*", 
patterns.get(1).getFullPath());
+  }
+
+  @Test
+  public void testKeepFullyPreciseBranchesSeparatedWithManyTags() {
+    final int tagColumnNum = 32;
+    final List<SchemaFilter> firstBranch = new ArrayList<>(tagColumnNum);
+    final List<SchemaFilter> secondBranch = new ArrayList<>(tagColumnNum);
+    for (int i = 0; i < tagColumnNum; i++) {
+      firstBranch.add(precise(i, "value" + i));
+      secondBranch.add(precise(i, i == tagColumnNum - 1 ? "other" : "value" + 
i));
+    }
+
+    final List<PartialPath> patterns = convert(tagColumnNum, firstBranch, 
secondBranch);
+
+    Assert.assertEquals(2, patterns.size());
+    Assert.assertEquals("value31", patterns.get(0).getNodes()[PREFIX.length + 
31]);
+    Assert.assertEquals("other", patterns.get(1).getNodes()[PREFIX.length + 
31]);
+  }
+
+  @Test
+  public void testCompactPreciseBranchesAfterLongNonPrecisePrefix() {
+    final int tagColumnNum = 32;
+    final List<PartialPath> patterns =
+        convert(
+            tagColumnNum,
+            branch(precise(16, "common"), precise(31, "last1")),
+            branch(precise(16, "common"), precise(31, "last2")));
+
+    Assert.assertEquals(1, patterns.size());
+    final ExtendedPartialPath pattern = (ExtendedPartialPath) patterns.get(0);
+    Assert.assertTrue(pattern.match(PREFIX.length + 16, "common"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 31, "last1"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 31, "last2"));
+    Assert.assertFalse(pattern.match(PREFIX.length + 31, "last3"));
+  }
+
+  @Test
+  public void testCompactCartesianProductBehindWildcard() {
+    final List<PartialPath> patterns =
+        convert(
+            branch(precise(1, "card1"), precise(2, "device1")),
+            branch(precise(1, "card1"), precise(2, "device2")),
+            branch(precise(1, "card2"), precise(2, "device1")),
+            branch(precise(1, "card2"), precise(2, "device2")));
+
+    Assert.assertEquals(1, patterns.size());
+    final ExtendedPartialPath pattern = (ExtendedPartialPath) patterns.get(0);
+    Assert.assertTrue(pattern.match(PREFIX.length + 1, "card1"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 1, "card2"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 2, "device1"));
+    Assert.assertTrue(pattern.match(PREFIX.length + 2, "device2"));
+  }
+
+  @Test
+  public void testDoNotCompactCorrelatedOrBranches() {
+    final List<PartialPath> patterns =
+        convert(
+            branch(precise(1, "card1"), precise(2, "device1")),
+            branch(precise(1, "card2"), precise(2, "device2")));
+
+    Assert.assertEquals(2, patterns.size());
+  }
+
+  @Test
+  public void testDoNotCompactNullPreciseValue() {
+    final List<PartialPath> patterns =
+        convert(branch(precise(1, null)), branch(precise(1, "card1")));
+
+    Assert.assertEquals(2, patterns.size());
+  }
+
+  @Test
+  public void testCombineMultiExactAndOtherFilters() {
+    final List<PartialPath> patterns =
+        convert(
+            branch(
+                new TagFilter(new InFilter(new 
HashSet<>(Arrays.asList("card1", "card2"))), 1),
+                new TagFilter(new NotFilter(new PreciseFilter("card2")), 1)));
+
+    Assert.assertEquals(1, patterns.size());
+    assertPreciseTransitionsAfterFirstTag(patterns.get(0), "card1");
+  }
+
+  @Test
+  public void testIterateMultiExactValuesWhenTheyAreFewer() {
+    final TestNode root = new TestNode("root");
+    final TestNode parent = new TestNode("meter");
+    root.addChild(parent);
+    parent.addChildren("card1", "card2", "card3", "card4");
+
+    final TestVisitor visitor =
+        new TestVisitor(root, createAdaptivePattern(Set.of("card1", "card2")), 
parent);
+
+    Assert.assertEquals(Arrays.asList("card1", "card2"), collect(visitor));
+    Assert.assertEquals(2, visitor.getTargetDirectLookupCount());
+    Assert.assertEquals(0, visitor.getTargetChildrenIterationCount());
+  }
+
+  @Test
+  public void testIterateChildKeysWhenTheyAreFewer() {
+    final TestNode root = new TestNode("root");
+    final TestNode parent = new TestNode("meter");
+    root.addChild(parent);
+    parent.addChildren("card1", "card2");
+
+    final TestVisitor visitor =
+        new TestVisitor(
+            root,
+            createAdaptivePattern(Set.of("card1", "card2", "card3", "card4", 
"card5")),
+            parent);
+
+    Assert.assertEquals(Arrays.asList("card1", "card2"), collect(visitor));
+    Assert.assertEquals(0, visitor.getTargetDirectLookupCount());
+    Assert.assertEquals(1, visitor.getTargetChildrenIterationCount());
+  }
+
+  @Test
+  public void testIterateChildKeysWhenCandidateCountsAreEqual() {
+    final TestNode root = new TestNode("root");
+    final TestNode parent = new TestNode("meter");
+    root.addChild(parent);
+    parent.addChildren("card1", "card2");
+
+    final TestVisitor visitor =
+        new TestVisitor(root, createAdaptivePattern(Set.of("card1", "card2")), 
parent);
+
+    Assert.assertEquals(Arrays.asList("card1", "card2"), collect(visitor));
+    Assert.assertEquals(0, visitor.getTargetDirectLookupCount());
+    Assert.assertEquals(1, visitor.getTargetChildrenIterationCount());
+  }
+
+  private static ExtendedPartialPath createAdaptivePattern(final Set<String> 
values) {
+    final ExtendedPartialPath pattern =
+        new ExtendedPartialPath(new String[] {"root", "*", "*"}, true);
+    pattern.addMultiExactMatch(2, values);
+    return pattern;
+  }
+
+  private static List<String> collect(final TestVisitor visitor) {
+    final List<String> result = new ArrayList<>();
+    try {
+      while (visitor.hasNext()) {
+        result.add(visitor.next());
+      }
+    } finally {
+      visitor.close();
+    }
+    Collections.sort(result);
+    return result;
+  }
+
+  @SafeVarargs
+  private static List<PartialPath> convert(final List<SchemaFilter>... 
branches) {
+    return convert(TAG_COLUMN_NUM, branches);
+  }
+
+  @SafeVarargs
+  private static List<PartialPath> convert(
+      final int tagColumnNum, final List<SchemaFilter>... branches) {
+    return DeviceFilterUtil.convertToDevicePattern(
+        PREFIX, tagColumnNum, Arrays.asList(branches), false);
+  }
+
+  private static List<SchemaFilter> branch(final SchemaFilter... filters) {
+    return Arrays.asList(filters);
+  }
+
+  private static TagFilter precise(final int index, final String value) {
+    return new TagFilter(new PreciseFilter(value), index);
+  }
+
+  private static void assertPreciseTransitionsAfterFirstTag(
+      final PartialPath pattern, final String... expectedTransitions) {
+    final IPatternFA patternFA = new 
IPatternFA.Builder().pattern(pattern).buildNFA();
+    IFAState state = patternFA.getInitialState();
+    for (final String prefixNode : PREFIX) {
+      final IFATransition transition = 
patternFA.getPreciseMatchTransition(state).get(prefixNode);
+      Assert.assertNotNull(transition);
+      state = patternFA.getNextState(state, transition);
+    }
+    final IFATransition wildcardTransition =
+        patternFA.getFuzzyMatchTransitionIterator(state).next();
+    state = patternFA.getNextState(state, wildcardTransition);
+
+    final Set<String> expected = new 
HashSet<>(Arrays.asList(expectedTransitions));
+    Assert.assertEquals(expected, 
patternFA.getPreciseMatchTransition(state).keySet());
+    Assert.assertEquals(0, patternFA.getFuzzyMatchTransitionSize(state));
+  }
+
+  private static class TestVisitor extends AbstractTreeVisitor<TestNode, 
String> {
+
+    private final TestNode targetParent;
+    private int targetDirectLookupCount;
+    private int targetChildrenIterationCount;
+
+    private TestVisitor(
+        final TestNode root, final ExtendedPartialPath pathPattern, final 
TestNode targetParent) {
+      super(root, pathPattern, false);
+      this.targetParent = targetParent;
+      initStack();
+    }
+
+    @Override
+    protected TestNode getChild(final TestNode parent, final String childName) 
{
+      if (parent == targetParent) {
+        targetDirectLookupCount++;
+      }
+      return parent.children.get(childName);
+    }
+
+    @Override
+    protected Iterator<TestNode> getChildrenIterator(final TestNode parent) {
+      if (parent == targetParent) {
+        targetChildrenIterationCount++;
+      }
+      return parent.children.values().iterator();
+    }
+
+    @Override
+    protected Iterator<TestNode> getChildrenIterator(
+        final TestNode parent, final Iterator<String> childrenName) {
+      final List<TestNode> children = new ArrayList<>();
+      childrenName.forEachRemaining(
+          name -> {
+            final TestNode child = parent.children.get(name);
+            if (child != null) {
+              children.add(child);
+            }
+          });
+      return children.iterator();
+    }
+
+    @Override
+    protected int getChildrenSize(final TestNode parent) {
+      return parent.children.keySet().size();
+    }
+
+    @Override
+    protected boolean shouldVisitSubtreeOfInternalMatchedNode(final TestNode 
node) {
+      return true;
+    }
+
+    @Override
+    protected boolean shouldVisitSubtreeOfFullMatchedNode(final TestNode node) 
{
+      return false;
+    }
+
+    @Override
+    protected boolean acceptInternalMatchedNode(final TestNode node) {
+      return false;
+    }
+
+    @Override
+    protected boolean acceptFullMatchedNode(final TestNode node) {
+      return true;
+    }
+
+    @Override
+    protected String generateResult(final TestNode nextMatchedNode) {
+      return nextMatchedNode.getName();
+    }
+
+    @Override
+    protected boolean mayTargetNodeType(final TestNode node) {
+      return true;
+    }
+
+    private int getTargetDirectLookupCount() {
+      return targetDirectLookupCount;
+    }
+
+    private int getTargetChildrenIterationCount() {
+      return targetChildrenIterationCount;
+    }
+  }
+
+  private static class TestNode implements ITreeNode {
+
+    private final String name;
+    private final Map<String, TestNode> children = new LinkedHashMap<>();
+
+    private TestNode(final String name) {
+      this.name = name;
+    }
+
+    private void addChild(final TestNode child) {
+      children.put(child.getName(), child);
+    }
+
+    private void addChildren(final String... childNames) {
+      Arrays.stream(childNames).map(TestNode::new).forEach(this::addChild);
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+  }
+}

Reply via email to