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

jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new 7419a4797ec Optimize tag index timeseries lookup (#18753) (#18781)
7419a4797ec is described below

commit 7419a4797ecf7cd71c26aff2fe81de7de2b3c320
Author: Caideyipi <[email protected]>
AuthorDate: Wed Sep 30 18:56:42 2026 +0800

    Optimize tag index timeseries lookup (#18753) (#18781)
    
    * Optimize tag index timeseries lookup
    
    * Add opt-in tag index performance test
    
    (cherry picked from commit 0c9a5ee09164b8cd35beb34e4dad75af54c6feae)
---
 .../schemaengine/schemaregion/tag/TagManager.java  |  54 ++---
 .../tag/TagManagerPerformanceTest.java             | 258 +++++++++++++++++++++
 .../schemaregion/tag/TagManagerTest.java           |  43 ++++
 3 files changed, 328 insertions(+), 27 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManager.java
index 805421bb504..1d8919dadcc 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManager.java
@@ -244,7 +244,8 @@ public class TagManager {
     return tagValueMap != null && tagValueMap.containsKey(tagValue);
   }
 
-  private List<IMeasurementMNode<?>> getMatchedTimeseriesInIndex(TagFilter 
tagFilter) {
+  List<IMeasurementMNode<?>> getMatchedTimeseriesInIndex(
+      final TagFilter tagFilter, final PartialPath pathPattern, final boolean 
isPrefixMatch) {
     Map<String, Set<IMeasurementMNode<?>>> value2Node = 
tagIndex.get(tagFilter.getKey());
     if (value2Node == null || value2Node.isEmpty()) {
       return Collections.emptyList();
@@ -262,19 +263,20 @@ public class TagManager {
         }
       }
     } else {
-      for (Map.Entry<String, Set<IMeasurementMNode<?>>> entry : 
value2Node.entrySet()) {
-        if (entry.getKey() == null || entry.getValue() == null) {
-          continue;
-        }
-        String tagValue = entry.getKey();
-        if (tagFilter.getValue().equals(tagValue)) {
-          allMatchedNodes.addAll(entry.getValue());
-        }
+      final Set<IMeasurementMNode<?>> matchedNodes = 
value2Node.get(tagFilter.getValue());
+      if (matchedNodes != null) {
+        allMatchedNodes.addAll(matchedNodes);
       }
     }
-    // we just sort them by the alphabetical order
+
+    // Filter by path before sorting to avoid sorting irrelevant measurements.
     allMatchedNodes =
         allMatchedNodes.stream()
+            .filter(
+                node ->
+                    isPrefixMatch
+                        ? 
pathPattern.prefixMatchFullPath(node.getPartialPath())
+                        : pathPattern.matchFullPath(node.getPartialPath()))
             .sorted(Comparator.comparing(IMNode::getFullPath))
             .collect(toList());
 
@@ -287,11 +289,13 @@ public class TagManager {
     SchemaFilter schemaFilter = plan.getSchemaFilter();
     // currently, only one TagFilter is supported
     // all IMeasurementMNode in allMatchedNodes satisfied TagFilter
+    PartialPath pathPattern = plan.getPath();
     Iterator<IMeasurementMNode<?>> allMatchedNodes =
         getMatchedTimeseriesInIndex(
-                (TagFilter) SchemaFilter.extract(schemaFilter, 
SchemaFilterType.TAGS_FILTER).get(0))
+                (TagFilter) SchemaFilter.extract(schemaFilter, 
SchemaFilterType.TAGS_FILTER).get(0),
+                pathPattern,
+                plan.isPrefixMatch())
             .iterator();
-    PartialPath pathPattern = plan.getPath();
     SchemaIterator<ITimeSeriesSchemaInfo> schemaIterator =
         new SchemaIterator<ITimeSeriesSchemaInfo>() {
           private ITimeSeriesSchemaInfo nextMatched;
@@ -323,21 +327,17 @@ public class TagManager {
             nextMatched = null;
             while (allMatchedNodes.hasNext()) {
               IMeasurementMNode<?> node = allMatchedNodes.next();
-              if (plan.isPrefixMatch()
-                  ? pathPattern.prefixMatchFullPath(node.getPartialPath())
-                  : pathPattern.matchFullPath(node.getPartialPath())) {
-                Pair<Map<String, String>, Map<String, String>> 
tagAndAttributePair =
-                    readTagFile(node.getOffset());
-                nextMatched =
-                    new ShowTimeSeriesResult(
-                        node.getFullPath(),
-                        node.getAlias(),
-                        node.getSchema(),
-                        tagAndAttributePair.left,
-                        tagAndAttributePair.right,
-                        node.getParent().getAsDeviceMNode().isAligned());
-                break;
-              }
+              Pair<Map<String, String>, Map<String, String>> 
tagAndAttributePair =
+                  readTagFile(node.getOffset());
+              nextMatched =
+                  new ShowTimeSeriesResult(
+                      node.getFullPath(),
+                      node.getAlias(),
+                      node.getSchema(),
+                      tagAndAttributePair.left,
+                      tagAndAttributePair.right,
+                      node.getParent().getAsDeviceMNode().isAligned());
+              break;
             }
           }
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerPerformanceTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerPerformanceTest.java
new file mode 100644
index 00000000000..58fb783c73c
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerPerformanceTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.schemaengine.schemaregion.tag;
+
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.filter.impl.TagFilter;
+import org.apache.iotdb.commons.schema.node.IMNode;
+import org.apache.iotdb.commons.schema.node.role.IDeviceMNode;
+import org.apache.iotdb.commons.schema.node.role.IMeasurementMNode;
+import org.apache.iotdb.commons.schema.node.utils.IMNodeFactory;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.IMemMNode;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.loader.MNodeFactoryLoader;
+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.apache.commons.io.FileUtils;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class TagManagerPerformanceTest {
+  private static final String PREFIX = "iotdb.tag.index.perf.";
+  private static volatile List<IMeasurementMNode<?>> benchmarkBlackhole;
+
+  @Test
+  public void compareExactLookupAndPathFiltering() throws Exception {
+    Assume.assumeTrue(
+        "Manual performance UT: enable with -D" + PREFIX + "enabled=true",
+        Boolean.getBoolean(PREFIX + "enabled"));
+    Assume.assumeTrue(
+        "Current-thread CPU and allocation metrics are required.",
+        ManualPerformanceTestUtils.enableThreadMetrics());
+    final int distinctValues = Integer.getInteger(PREFIX + "distinctValues", 
10_000);
+    final int candidateNodes = Integer.getInteger(PREFIX + "candidateNodes", 
2_000);
+    final int matchingNodes = Integer.getInteger(PREFIX + "matchingNodes", 
100);
+    final int warmups = Integer.getInteger(PREFIX + "warmups", 30);
+    final int exactIterations = Integer.getInteger(PREFIX + "exactIterations", 
50_000);
+    final int pathIterations = Integer.getInteger(PREFIX + "pathIterations", 
1_000);
+    final int rounds = Integer.getInteger(PREFIX + "rounds", 5);
+    Assert.assertTrue(
+        distinctValues > 0
+            && candidateNodes > 0
+            && matchingNodes > 0
+            && matchingNodes <= candidateNodes);
+    Assert.assertTrue(warmups >= 0 && exactIterations > 0 && pathIterations > 
0 && rounds > 0);
+
+    final File directory = 
Files.createTempDirectory("tag-index-performance").toFile();
+    try {
+      final TagManager manager = new TagManager(directory.getAbsolutePath(), 
null);
+      try {
+        final IMNodeFactory<IMemMNode> factory =
+            MNodeFactoryLoader.getInstance().getMemMNodeIMNodeFactory();
+        final IMemMNode root = factory.createInternalMNode(null, "root");
+        final IDeviceMNode<IMemMNode> matchingDevice =
+            factory.createDeviceMNode(factory.createInternalMNode(root, "sg"), 
"d");
+        final IDeviceMNode<IMemMNode> otherDevice =
+            factory.createDeviceMNode(factory.createInternalMNode(root, 
"other"), "d");
+        final IMeasurementMNode<IMemMNode> shared =
+            newMeasurement(factory, matchingDevice, "single");
+        for (int i = 0; i < distinctValues; i++) {
+          manager.addIndex("cardinality", "v" + i, shared);
+        }
+        for (int i = 0; i < candidateNodes; i++) {
+          manager.addIndex(
+              "selectivity",
+              "target",
+              newMeasurement(factory, i < matchingNodes ? matchingDevice : 
otherDevice, "s" + i));
+        }
+        final Map<String, Map<String, Set<IMeasurementMNode<?>>>> index = 
getIndex(manager);
+        final PartialPath pattern = new PartialPath("root.sg.**");
+        System.out.printf(
+            Locale.ROOT,
+            "Tag index benchmark: distinctValues=%d, candidateNodes=%d, 
matchingNodes=%d, warmups=%d, exactIterations/round=%d, 
pathIterations/round=%d, rounds=%d%n",
+            distinctValues,
+            candidateNodes,
+            matchingNodes,
+            warmups,
+            exactIterations,
+            pathIterations,
+            rounds);
+        benchmark(
+            "exact value lookup",
+            manager,
+            index,
+            new TagFilter("cardinality", "v0", false),
+            pattern,
+            1,
+            warmups,
+            exactIterations,
+            rounds);
+        benchmark(
+            "path filtering before sort",
+            manager,
+            index,
+            new TagFilter("selectivity", "target", false),
+            pattern,
+            matchingNodes,
+            warmups,
+            pathIterations,
+            rounds);
+      } finally {
+        manager.clear();
+      }
+    } finally {
+      FileUtils.deleteDirectory(directory);
+    }
+  }
+
+  private static IMeasurementMNode<IMemMNode> newMeasurement(
+      IMNodeFactory<IMemMNode> factory, IDeviceMNode<IMemMNode> parent, String 
name) {
+    return factory.createMeasurementMNode(
+        parent,
+        name,
+        new MeasurementSchema(name, TSDataType.INT64, TSEncoding.PLAIN, 
CompressionType.SNAPPY),
+        null);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Map<String, Map<String, Set<IMeasurementMNode<?>>>> 
getIndex(TagManager manager)
+      throws Exception {
+    final Field field = TagManager.class.getDeclaredField("tagIndex");
+    field.setAccessible(true);
+    return (Map<String, Map<String, Set<IMeasurementMNode<?>>>>) 
field.get(manager);
+  }
+
+  // Reproduce the old value scan and sort, then apply the reader's path 
filter.
+  private static List<IMeasurementMNode<?>> legacyLookup(
+      Map<String, Map<String, Set<IMeasurementMNode<?>>>> index,
+      TagFilter filter,
+      PartialPath pattern) {
+    final Map<String, Set<IMeasurementMNode<?>>> values = 
index.get(filter.getKey());
+    if (values == null || values.isEmpty()) {
+      return Collections.emptyList();
+    }
+    final List<IMeasurementMNode<?>> candidates = new ArrayList<>();
+    for (Map.Entry<String, Set<IMeasurementMNode<?>>> entry : 
values.entrySet()) {
+      if (entry.getKey() != null
+          && entry.getValue() != null
+          && filter.getValue().equals(entry.getKey())) {
+        candidates.addAll(entry.getValue());
+      }
+    }
+    final List<IMeasurementMNode<?>> sorted =
+        candidates.stream()
+            .sorted(Comparator.comparing(IMNode::getFullPath))
+            .collect(Collectors.toList());
+    sorted.removeIf(node -> !pattern.matchFullPath(node.getPartialPath()));
+    return sorted;
+  }
+
+  private static void benchmark(
+      String label,
+      TagManager manager,
+      Map<String, Map<String, Set<IMeasurementMNode<?>>>> index,
+      TagFilter filter,
+      PartialPath pattern,
+      int expectedMatches,
+      int warmups,
+      int iterations,
+      int rounds) {
+    final List<IMeasurementMNode<?>> legacyResult = legacyLookup(index, 
filter, pattern);
+    final List<IMeasurementMNode<?>> optimizedResult =
+        manager.getMatchedTimeseriesInIndex(filter, pattern, false);
+    Assert.assertEquals(expectedMatches, optimizedResult.size());
+    Assert.assertEquals(legacyResult, optimizedResult);
+    final Runnable legacy = () -> benchmarkBlackhole = legacyLookup(index, 
filter, pattern);
+    final Runnable optimized =
+        () -> benchmarkBlackhole = manager.getMatchedTimeseriesInIndex(filter, 
pattern, false);
+    for (int i = 0; i < warmups; i++) {
+      if ((i & 1) == 0) {
+        legacy.run();
+        optimized.run();
+      } else {
+        optimized.run();
+        legacy.run();
+      }
+    }
+    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, 
legacy);
+        optimizedMeasurements[i] = 
ManualPerformanceTestUtils.measure(iterations, optimized);
+      } else {
+        optimizedMeasurements[i] = 
ManualPerformanceTestUtils.measure(iterations, optimized);
+        legacyMeasurements[i] = ManualPerformanceTestUtils.measure(iterations, 
legacy);
+      }
+    }
+    final Summary oldSummary = 
ManualPerformanceTestUtils.summarize(legacyMeasurements, iterations);
+    final Summary newSummary =
+        ManualPerformanceTestUtils.summarize(optimizedMeasurements, 
iterations);
+    System.out.printf(Locale.ROOT, "  %s (matches=%d):%n", label, 
expectedMatches);
+    printSummary("legacy", oldSummary);
+    printSummary("optimized", newSummary);
+    final double allocationReduction =
+        oldSummary.getAllocatedBytesPerOperation() == 0
+            ? 0
+            : (oldSummary.getAllocatedBytesPerOperation()
+                    - newSummary.getAllocatedBytesPerOperation())
+                * 100.0
+                / oldSummary.getAllocatedBytesPerOperation();
+    if (oldSummary.getCpuNanosPerOperation() > 0 && 
newSummary.getCpuNanosPerOperation() > 0) {
+      System.out.printf(
+          Locale.ROOT,
+          "    CPU speedup=%.2fx, allocation reduction=%.1f%%%n",
+          oldSummary.getCpuNanosPerOperation() / 
newSummary.getCpuNanosPerOperation(),
+          allocationReduction);
+    } else {
+      System.out.printf(
+          Locale.ROOT,
+          "    CPU speedup=n/a (timer resolution), allocation 
reduction=%.1f%%%n",
+          allocationReduction);
+    }
+  }
+
+  private static void printSummary(String label, Summary summary) {
+    System.out.printf(
+        Locale.ROOT,
+        "    %-9s CPU=%.3f us/query, allocated=%.1f B/query%n",
+        label,
+        summary.getCpuNanosPerOperation() / 1_000.0,
+        summary.getAllocatedBytesPerOperation());
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerTest.java
index 1d78b4a5483..51b6456eabb 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/tag/TagManagerTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.iotdb.db.schemaengine.schemaregion.tag;
 
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.filter.impl.TagFilter;
 import org.apache.iotdb.commons.schema.node.role.IMeasurementMNode;
 import org.apache.iotdb.commons.schema.node.utils.IMNodeFactory;
 import org.apache.iotdb.db.schemaengine.rescon.MemSchemaEngineStatistics;
@@ -34,11 +36,18 @@ import org.apache.tsfile.write.schema.MeasurementSchema;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 import java.io.File;
+import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -188,6 +197,33 @@ public class TagManagerTest {
     Assert.assertEquals(0, regionStatistics.getRegionMemoryUsage());
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void preciseQueryUsesDirectLookupAndFiltersPathBeforeSorting() throws 
Exception {
+    initTagManager();
+    final IMeasurementMNode<?> first = mockMeasurement("root.sg.d.s1");
+    final IMeasurementMNode<?> second = mockMeasurement("root.sg.d.s2");
+    final IMeasurementMNode<?> outside = mockMeasurement("root.other.d.s0");
+
+    final Map<String, Set<IMeasurementMNode<?>>> valueMap = Mockito.spy(new 
HashMap<>());
+    valueMap.put("target", new HashSet<>(Arrays.asList(second, outside, 
first)));
+    valueMap.put("unrelated", new HashSet<>());
+
+    final Field tagIndexField = TagManager.class.getDeclaredField("tagIndex");
+    tagIndexField.setAccessible(true);
+    final Map<String, Map<String, Set<IMeasurementMNode<?>>>> tagIndex =
+        (Map<String, Map<String, Set<IMeasurementMNode<?>>>>) 
tagIndexField.get(tagManager);
+    tagIndex.put("key", valueMap);
+
+    final List<IMeasurementMNode<?>> result =
+        tagManager.getMatchedTimeseriesInIndex(
+            new TagFilter("key", "target", false), new 
PartialPath("root.sg.**"), false);
+
+    Assert.assertEquals(Arrays.asList(first, second), result);
+    Mockito.verify(valueMap).get("target");
+    Mockito.verify(valueMap, Mockito.never()).entrySet();
+  }
+
   private void initTagManager() throws Exception {
     tempDir = Files.createTempDirectory("tag-manager").toFile();
     regionStatistics = new MemSchemaRegionStatistics(0, new 
MemSchemaEngineStatistics());
@@ -205,6 +241,13 @@ public class TagManagerTest {
         null);
   }
 
+  private static IMeasurementMNode<?> mockMeasurement(final String path) 
throws Exception {
+    final IMeasurementMNode<?> node = Mockito.mock(IMeasurementMNode.class);
+    Mockito.when(node.getFullPath()).thenReturn(path);
+    Mockito.when(node.getPartialPath()).thenReturn(new PartialPath(path));
+    return node;
+  }
+
   private static long indexMemory(
       final String tagKey, final String tagValue, final int measurementCount) {
     return RamUsageEstimator.sizeOf(tagKey)

Reply via email to