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 13faa155d32 Added flags to mark whether a device has device 
descendants to optimize query like select xx from xxx.** (#17672) (#17802)
13faa155d32 is described below

commit 13faa155d32fe4e27fab86edd8b673942dadf026
Author: Caideyipi <[email protected]>
AuthorDate: Tue Jun 2 18:00:42 2026 +0800

    Added flags to mark whether a device has device descendants to optimize 
query like select xx from xxx.** (#17672) (#17802)
    
    * flag-
    
    * Fix
    
    * Address device descendant flag review comments
    
    (cherry picked from commit a565a06825317e5f39393e619fb44e3fbbb88a9b)
---
 .../mtree/impl/mem/MTreeBelowSGMemoryImpl.java     | 130 +++++++++--
 .../mtree/impl/mem/mnode/IMemMNode.java            |  30 ++-
 .../mtree/impl/mem/mnode/basic/BasicMNode.java     |  34 ++-
 .../mtree/impl/pbtree/MTreeBelowSGCachedImpl.java  |  83 +++++--
 .../mtree/impl/pbtree/mnode/ICachedMNode.java      |  19 ++
 .../impl/pbtree/mnode/basic/CachedBasicMNode.java  |  40 +++-
 .../schemaregion/mtree/traverser/Traverser.java    | 102 +++++++++
 .../traverser/basic/MeasurementTraverser.java      |   5 +
 .../schemaRegion/SchemaRegionBasicTest.java        |  22 ++
 .../mtree/impl/mem/MTreeBelowSGMemoryImplTest.java | 253 +++++++++++++++++++++
 .../impl/pbtree/MTreeBelowSGCachedImplTest.java    | 217 ++++++++++++++++++
 .../node/common/AbstractAboveDatabaseMNode.java    |   4 +
 .../commons/schema/node/utils/IMNodeIterator.java  |   2 +-
 .../commons/schema/tree/AbstractTreeVisitor.java   |   4 +
 14 files changed, 908 insertions(+), 37 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
index b6f1221f701..a82250e2299 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
@@ -174,6 +174,59 @@ public class MTreeBelowSGMemoryImpl {
     return store.createSnapshot(snapshotDir);
   }
 
+  private void applySubtreeMeasurementDelta(IMemMNode startNode, final long 
delta) {
+    if (delta == 0 || startNode == null) {
+      return;
+    }
+    IMemMNode current = startNode;
+    while (current != null) {
+      current.setSubtreeMeasurementCount(current.getSubtreeMeasurementCount() 
+ delta);
+      current = current.getParent();
+    }
+  }
+
+  private IDeviceMNode<IMemMNode> setToEntityAndUpdateFlags(final IMemMNode 
node) {
+    final boolean wasDevice = node.isDevice();
+    final IDeviceMNode<IMemMNode> deviceMNode = store.setToEntity(node);
+    if (!wasDevice) {
+      markAncestorsHavingDeviceDescendant(node);
+    }
+    return deviceMNode;
+  }
+
+  private void markAncestorsHavingDeviceDescendant(final IMemMNode deviceNode) 
{
+    IMemMNode current = deviceNode.getParent();
+    while (current != null && !current.hasDeviceDescendant()) {
+      current.setHasDeviceDescendant(true);
+      current = current.getParent();
+    }
+  }
+
+  private boolean hasDeviceDescendantInChildren(final IMemMNode node) {
+    try (final IMNodeIterator<IMemMNode> iterator = 
store.getChildrenIterator(node)) {
+      while (iterator.hasNext()) {
+        final IMemMNode child = iterator.next();
+        if (child.isDevice() || child.hasDeviceDescendant()) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  private void refreshAncestorsHavingDeviceDescendant(IMemMNode startNode) {
+    IMemMNode current = startNode;
+    while (current != null) {
+      current.setHasDeviceDescendant(hasDeviceDescendantInChildren(current));
+      current = current.getParent();
+    }
+  }
+
+  private long getTemplateMeasurementCount(final int templateId) {
+    final Template template = 
ClusterTemplateManager.getInstance().getTemplate(templateId);
+    return template == null ? 0L : template.getMeasurementNumber();
+  }
+
   public static MTreeBelowSGMemoryImpl loadFromSnapshot(
       File snapshotDir,
       String storageGroupFullPath,
@@ -184,13 +237,16 @@ public class MTreeBelowSGMemoryImpl {
       Function<IMeasurementMNode<IMemMNode>, Map<String, String>> tagGetter,
       Function<IMeasurementMNode<IMemMNode>, Map<String, String>> 
attributeGetter)
       throws IOException, IllegalPathException {
-    return new MTreeBelowSGMemoryImpl(
-        new PartialPath(storageGroupFullPath),
-        MemMTreeStore.loadFromSnapshot(
-            snapshotDir, measurementProcess, deviceProcess, regionStatistics, 
metric),
-        tagGetter,
-        attributeGetter,
-        regionStatistics);
+    final MTreeBelowSGMemoryImpl mtree =
+        new MTreeBelowSGMemoryImpl(
+            new PartialPath(storageGroupFullPath),
+            MemMTreeStore.loadFromSnapshot(
+                snapshotDir, measurementProcess, deviceProcess, 
regionStatistics, metric),
+            tagGetter,
+            attributeGetter,
+            regionStatistics);
+    mtree.rebuildSubtreeMeasurementCount();
+    return mtree;
   }
 
   // endregion
@@ -268,7 +324,7 @@ public class MTreeBelowSGMemoryImpl {
       if (device.isDevice()) {
         entityMNode = device.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(device);
+        entityMNode = setToEntityAndUpdateFlags(device);
       }
 
       // create a non-aligned time series
@@ -290,6 +346,7 @@ public class MTreeBelowSGMemoryImpl {
         entityMNode.addAlias(alias, measurementMNode);
       }
 
+      applySubtreeMeasurementDelta(measurementMNode.getAsMNode(), 1L);
       return measurementMNode;
     }
   }
@@ -360,7 +417,7 @@ public class MTreeBelowSGMemoryImpl {
       if (device.isDevice()) {
         entityMNode = device.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(device);
+        entityMNode = setToEntityAndUpdateFlags(device);
         entityMNode.setAligned(true);
       }
 
@@ -386,6 +443,7 @@ public class MTreeBelowSGMemoryImpl {
         if (aliasList != null && aliasList.get(i) != null) {
           entityMNode.addAlias(aliasList.get(i), measurementMNode);
         }
+        applySubtreeMeasurementDelta(measurementMNode.getAsMNode(), 1L);
         measurementMNodeList.add(measurementMNode);
       }
       return measurementMNodeList;
@@ -539,6 +597,7 @@ public class MTreeBelowSGMemoryImpl {
       if (deletedNode.getAlias() != null) {
         parent.getAsDeviceMNode().deleteAliasChild(deletedNode.getAlias());
       }
+      applySubtreeMeasurementDelta(parent, -1L);
     }
     deleteEmptyInternalMNode(parent.getAsDeviceMNode());
     return deletedNode;
@@ -551,14 +610,15 @@ public class MTreeBelowSGMemoryImpl {
       boolean hasMeasurement = false;
       boolean hasNonViewMeasurement = false;
       IMemMNode child;
-      IMNodeIterator<IMemMNode> iterator = store.getChildrenIterator(curNode);
-      while (iterator.hasNext()) {
-        child = iterator.next();
-        if (child.isMeasurement()) {
-          hasMeasurement = true;
-          if (!child.getAsMeasurementMNode().isLogicalView()) {
-            hasNonViewMeasurement = true;
-            break;
+      try (final IMNodeIterator<IMemMNode> iterator = 
store.getChildrenIterator(curNode)) {
+        while (iterator.hasNext()) {
+          child = iterator.next();
+          if (child.isMeasurement()) {
+            hasMeasurement = true;
+            if (!child.getAsMeasurementMNode().isLogicalView()) {
+              hasNonViewMeasurement = true;
+              break;
+            }
           }
         }
       }
@@ -567,6 +627,7 @@ public class MTreeBelowSGMemoryImpl {
         synchronized (this) {
           curNode = store.setToInternal(entityMNode);
         }
+        refreshAncestorsHavingDeviceDescendant(curNode.getParent());
       } else if (!hasNonViewMeasurement) {
         // has some measurement but they are all logical view
         entityMNode.setAligned(null);
@@ -889,7 +950,7 @@ public class MTreeBelowSGMemoryImpl {
       if (cur.isDevice()) {
         entityMNode = cur.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(cur);
+        entityMNode = setToEntityAndUpdateFlags(cur);
       }
     }
 
@@ -907,6 +968,7 @@ public class MTreeBelowSGMemoryImpl {
     entityMNode.setUseTemplate(true);
     entityMNode.setSchemaTemplateId(template.getId());
     regionStatistics.activateTemplate(template.getId());
+    applySubtreeMeasurementDelta(entityMNode.getAsMNode(), (long) 
template.getMeasurementNumber());
   }
 
   public Map<PartialPath, List<Integer>> constructSchemaBlackListWithTemplate(
@@ -968,6 +1030,8 @@ public class MTreeBelowSGMemoryImpl {
                 resultTemplateSetInfo.put(
                     node.getPartialPath(), 
Collections.singletonList(node.getSchemaTemplateId()));
                 
regionStatistics.deactivateTemplate(node.getSchemaTemplateId());
+                applySubtreeMeasurementDelta(
+                    node.getAsMNode(), 
-getTemplateMeasurementCount(node.getSchemaTemplateId()));
                 node.deactivateTemplate();
                 deleteEmptyInternalMNode(node);
               }
@@ -991,7 +1055,7 @@ public class MTreeBelowSGMemoryImpl {
     if (cur.isDevice()) {
       entityMNode = cur.getAsDeviceMNode();
     } else {
-      entityMNode = store.setToEntity(cur);
+      entityMNode = setToEntityAndUpdateFlags(cur);
     }
 
     if (!entityMNode.isAligned()) {
@@ -1000,6 +1064,7 @@ public class MTreeBelowSGMemoryImpl {
     entityMNode.setUseTemplate(true);
     entityMNode.setSchemaTemplateId(templateId);
     regionStatistics.activateTemplate(templateId);
+    applySubtreeMeasurementDelta(entityMNode.getAsMNode(), 
getTemplateMeasurementCount(templateId));
   }
 
   public long countPathsUsingTemplate(PartialPath pathPattern, int templateId)
@@ -1011,6 +1076,30 @@ public class MTreeBelowSGMemoryImpl {
     }
   }
 
+  public void rebuildSubtreeMeasurementCount() {
+    rebuildSubtreeMeasurementCountFromNode(rootNode);
+  }
+
+  private long rebuildSubtreeMeasurementCountFromNode(final IMemMNode node) {
+    long count = node.isMeasurement() ? 1L : 0L;
+    boolean hasDeviceDescendant = false;
+    try (final IMNodeIterator<IMemMNode> iterator = 
store.getChildrenIterator(node)) {
+      while (iterator.hasNext()) {
+        final IMemMNode child = iterator.next();
+        count += rebuildSubtreeMeasurementCountFromNode(child);
+        if (child.isDevice() || child.hasDeviceDescendant()) {
+          hasDeviceDescendant = true;
+        }
+      }
+    }
+    if (node.isDevice() && node.getAsDeviceMNode().isUseTemplate()) {
+      count += 
getTemplateMeasurementCount(node.getAsDeviceMNode().getSchemaTemplateId());
+    }
+    node.setSubtreeMeasurementCount(count);
+    node.setHasDeviceDescendant(hasDeviceDescendant);
+    return count;
+  }
+
   // endregion
 
   // region Interfaces for schema reader
@@ -1278,7 +1367,7 @@ public class MTreeBelowSGMemoryImpl {
       if (device.isDevice()) {
         entityMNode = device.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(device);
+        entityMNode = setToEntityAndUpdateFlags(device);
         // this parent has no measurement before. The leafName is his first 
child who is a logical
         // view.
         entityMNode.setAligned(null);
@@ -1287,6 +1376,7 @@ public class MTreeBelowSGMemoryImpl {
       measurementMNode.setParent(entityMNode.getAsMNode());
       store.addChild(entityMNode.getAsMNode(), leafName, 
measurementMNode.getAsMNode());
 
+      applySubtreeMeasurementDelta(measurementMNode.getAsMNode(), 1L);
       return measurementMNode;
     }
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/IMemMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/IMemMNode.java
index d3d055928b1..257d7caeef8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/IMemMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/IMemMNode.java
@@ -19,5 +19,33 @@
 package org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode;
 
 import org.apache.iotdb.commons.schema.node.IMNode;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.basic.BasicMNode;
 
-public interface IMemMNode extends IMNode<IMemMNode> {}
+public interface IMemMNode extends IMNode<IMemMNode> {
+
+  BasicMNode getBasicMNode();
+
+  /**
+   * The count of measurement nodes contained in the subtree rooted at this 
node. The counter is
+   * maintained in memory only.
+   */
+  default long getSubtreeMeasurementCount() {
+    return getBasicMNode().getSubtreeMeasurementCount();
+  }
+
+  default void setSubtreeMeasurementCount(final long subtreeMeasurementCount) {
+    getBasicMNode().setSubtreeMeasurementCount(subtreeMeasurementCount);
+  }
+
+  /**
+   * Whether there is any device node in the subtree rooted at this node, 
excluding the node itself.
+   * This flag is maintained in memory only.
+   */
+  default boolean hasDeviceDescendant() {
+    return getBasicMNode().hasDeviceDescendant();
+  }
+
+  default void setHasDeviceDescendant(final boolean hasDeviceDescendant) {
+    getBasicMNode().setHasDeviceDescendant(hasDeviceDescendant);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/basic/BasicMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/basic/BasicMNode.java
index ccacef6c7eb..e70885c7d03 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/basic/BasicMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/basic/BasicMNode.java
@@ -46,6 +46,12 @@ public class BasicMNode implements IMemMNode {
   private IMemMNode parent;
   private final BasicMNodeInfo basicMNodeInfo;
 
+  /** Cached count of measurements in this node's subtree, rebuilt on restart. 
*/
+  private long subtreeMeasurementCount = 0L;
+
+  /** Cached flag showing whether there is any device in the subtree below 
this node. */
+  private boolean hasDeviceDescendant = false;
+
   /** from root to this node, only be set when used once for InternalMNode */
   private String fullPath;
 
@@ -55,6 +61,11 @@ public class BasicMNode implements IMemMNode {
     this.basicMNodeInfo = new BasicMNodeInfo(name);
   }
 
+  @Override
+  public BasicMNode getBasicMNode() {
+    return this;
+  }
+
   @Override
   public String getName() {
     return basicMNodeInfo.getName();
@@ -98,6 +109,25 @@ public class BasicMNode implements IMemMNode {
     this.fullPath = fullPath;
   }
 
+  public long getSubtreeMeasurementCount() {
+    return subtreeMeasurementCount;
+  }
+
+  @Override
+  public void setSubtreeMeasurementCount(final long subtreeMeasurementCount) {
+    this.subtreeMeasurementCount = subtreeMeasurementCount;
+  }
+
+  @Override
+  public boolean hasDeviceDescendant() {
+    return hasDeviceDescendant;
+  }
+
+  @Override
+  public void setHasDeviceDescendant(final boolean hasDeviceDescendant) {
+    this.hasDeviceDescendant = hasDeviceDescendant;
+  }
+
   @Override
   public PartialPath getPartialPath() {
     List<String> detachedPath = new ArrayList<>();
@@ -224,6 +254,8 @@ public class BasicMNode implements IMemMNode {
    *         <li>basicMNodeInfo reference, 8B
    *         <li>parent reference, 8B
    *         <li>fullPath reference, 8B
+   *         <li>subtreeMeasurementCount, 8B
+   *         <li>hasDeviceDescendant, 1B
    *       </ol>
    *   <li>MapEntry in parent
    *       <ol>
@@ -235,7 +267,7 @@ public class BasicMNode implements IMemMNode {
    */
   @Override
   public int estimateSize() {
-    return 8 + 8 + 8 + 8 + 8 + 8 + 28 + basicMNodeInfo.estimateSize();
+    return 8 + 8 + 8 + 8 + 8 + 1 + 8 + 8 + 28 + basicMNodeInfo.estimateSize();
   }
 
   @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
index f79a8c2e057..829d242187e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
@@ -132,6 +132,68 @@ public class MTreeBelowSGCachedImpl {
   private final int levelOfDB;
   private final CachedSchemaRegionStatistics regionStatistics;
 
+  private IDeviceMNode<ICachedMNode> setToEntityAndUpdateFlags(final 
ICachedMNode node)
+      throws MetadataException {
+    final boolean wasDevice = node.isDevice();
+    if (!node.isDeviceDescendantComputed()) {
+      node.setHasDeviceDescendant(hasDeviceDescendantInChildren(node));
+      node.setDeviceDescendantComputed(true);
+    }
+    final IDeviceMNode<ICachedMNode> deviceMNode = store.setToEntity(node);
+    if (!wasDevice) {
+      markAncestorsHavingDeviceDescendant(node);
+    }
+    return deviceMNode;
+  }
+
+  private void markAncestorsHavingDeviceDescendant(final ICachedMNode 
deviceNode) {
+    ICachedMNode current = deviceNode.getParent();
+    while (current != null && !current.hasDeviceDescendant()) {
+      current.setHasDeviceDescendant(true);
+      current.setDeviceDescendantComputed(true);
+      current = current.getParent();
+    }
+  }
+
+  private boolean hasDeviceDescendantInChildren(final ICachedMNode node) 
throws MetadataException {
+    try (final IMNodeIterator<ICachedMNode> iterator = 
store.getChildrenIterator(node)) {
+      while (iterator.hasNext()) {
+        final ICachedMNode child = iterator.next();
+        try {
+          if (child.isDevice() || hasDeviceDescendant(child)) {
+            return true;
+          }
+        } finally {
+          unPinMNode(child);
+        }
+      }
+      return false;
+    }
+  }
+
+  private boolean hasDeviceDescendant(final ICachedMNode node) throws 
MetadataException {
+    if (node.isMeasurement()) {
+      node.setHasDeviceDescendant(false);
+      node.setDeviceDescendantComputed(true);
+      return false;
+    }
+    if (!node.isDeviceDescendantComputed()) {
+      node.setHasDeviceDescendant(hasDeviceDescendantInChildren(node));
+      node.setDeviceDescendantComputed(true);
+    }
+    return node.hasDeviceDescendant();
+  }
+
+  private void refreshAncestorsHavingDeviceDescendant(ICachedMNode startNode)
+      throws MetadataException {
+    ICachedMNode current = startNode;
+    while (current != null) {
+      current.setHasDeviceDescendant(hasDeviceDescendantInChildren(current));
+      current.setDeviceDescendantComputed(true);
+      current = current.getParent();
+    }
+  }
+
   // region MTree initialization, clear and serialization
   public MTreeBelowSGCachedImpl(
       PartialPath storageGroupPath,
@@ -348,7 +410,7 @@ public class MTreeBelowSGCachedImpl {
           if (device.isDevice()) {
             entityMNode = device.getAsDeviceMNode();
           } else {
-            entityMNode = store.setToEntity(device);
+            entityMNode = setToEntityAndUpdateFlags(device);
             device = entityMNode.getAsMNode();
           }
 
@@ -453,7 +515,7 @@ public class MTreeBelowSGCachedImpl {
           if (device.isDevice()) {
             entityMNode = device.getAsDeviceMNode();
           } else {
-            entityMNode = store.setToEntity(device);
+            entityMNode = setToEntityAndUpdateFlags(device);
             entityMNode.setAligned(true);
             device = entityMNode.getAsMNode();
           }
@@ -674,8 +736,7 @@ public class MTreeBelowSGCachedImpl {
         boolean hasMeasurement = false;
         boolean hasNonViewMeasurement = false;
         ICachedMNode child;
-        IMNodeIterator<ICachedMNode> iterator = 
store.getChildrenIterator(curNode);
-        try {
+        try (final IMNodeIterator<ICachedMNode> iterator = 
store.getChildrenIterator(curNode)) {
           while (iterator.hasNext()) {
             child = iterator.next();
             unPinMNode(child);
@@ -687,12 +748,11 @@ public class MTreeBelowSGCachedImpl {
               }
             }
           }
-        } finally {
-          iterator.close();
         }
 
         if (!hasMeasurement) {
           curNode = store.setToInternal(entityMNode);
+          refreshAncestorsHavingDeviceDescendant(curNode.getParent());
         } else if (!hasNonViewMeasurement) {
           // has some measurement but they are all logical view
           store.updateMNode(entityMNode.getAsMNode(), o -> 
o.getAsDeviceMNode().setAligned(null));
@@ -719,14 +779,11 @@ public class MTreeBelowSGCachedImpl {
   }
 
   private boolean isEmptyInternalMNode(ICachedMNode node) throws 
MetadataException {
-    IMNodeIterator<ICachedMNode> iterator = store.getChildrenIterator(node);
-    try {
+    try (final IMNodeIterator<ICachedMNode> iterator = 
store.getChildrenIterator(node)) {
       return !IoTDBConstant.PATH_ROOT.equals(node.getName())
           && !node.isMeasurement()
           && !(node.isDevice() && node.getAsDeviceMNode().isUseTemplate())
           && !iterator.hasNext();
-    } finally {
-      iterator.close();
     }
   }
 
@@ -1064,7 +1121,7 @@ public class MTreeBelowSGCachedImpl {
           if (device.isDevice()) {
             entityMNode = device.getAsDeviceMNode();
           } else {
-            entityMNode = store.setToEntity(device);
+            entityMNode = setToEntityAndUpdateFlags(device);
             // this parent has no measurement before. The leafName is his 
first child who is a
             // logical
             // view.
@@ -1186,7 +1243,7 @@ public class MTreeBelowSGCachedImpl {
         if (cur.isDevice()) {
           entityMNode = cur.getAsDeviceMNode();
         } else {
-          entityMNode = store.setToEntity(cur);
+          entityMNode = setToEntityAndUpdateFlags(cur);
         }
 
         if (entityMNode.isUseTemplate()) {
@@ -1232,7 +1289,7 @@ public class MTreeBelowSGCachedImpl {
       if (cur.isDevice()) {
         entityMNode = cur.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(cur);
+        entityMNode = setToEntityAndUpdateFlags(cur);
       }
 
       store.updateMNode(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/ICachedMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/ICachedMNode.java
index 489b1a642aa..244e077013c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/ICachedMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/ICachedMNode.java
@@ -22,8 +22,27 @@ package 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.mnode;
 import org.apache.iotdb.commons.schema.node.IMNode;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.lock.LockEntry;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.memory.cache.CacheEntry;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.mnode.basic.CachedBasicMNode;
 
 public interface ICachedMNode extends IMNode<ICachedMNode> {
+  CachedBasicMNode getBasicMNode();
+
+  default boolean hasDeviceDescendant() {
+    return getBasicMNode().hasDeviceDescendant();
+  }
+
+  default void setHasDeviceDescendant(final boolean hasDeviceDescendant) {
+    getBasicMNode().setHasDeviceDescendant(hasDeviceDescendant);
+  }
+
+  default boolean isDeviceDescendantComputed() {
+    return getBasicMNode().isDeviceDescendantComputed();
+  }
+
+  default void setDeviceDescendantComputed(final boolean 
deviceDescendantComputed) {
+    getBasicMNode().setDeviceDescendantComputed(deviceDescendantComputed);
+  }
+
   CacheEntry getCacheEntry();
 
   void setCacheEntry(CacheEntry cacheEntry);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/basic/CachedBasicMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/basic/CachedBasicMNode.java
index d34fde474ab..14bb9f3609b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/basic/CachedBasicMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/mnode/basic/CachedBasicMNode.java
@@ -47,6 +47,17 @@ public class CachedBasicMNode implements ICachedMNode {
   private ICachedMNode parent;
   private final CacheMNodeInfo cacheMNodeInfo;
 
+  /** Cached flag showing whether there is any device in the subtree below 
this node. */
+  private boolean hasDeviceDescendant = false;
+
+  /**
+   * Whether {@link #hasDeviceDescendant} is trusted for the current in-memory 
node instance.
+   *
+   * <p>This state is intentionally not persisted. A node reloaded from PBTree 
can lazily recompute
+   * it when the wildcard-suffix optimization needs it.
+   */
+  private boolean deviceDescendantComputed = false;
+
   /** from root to this node, only be set when used once for InternalMNode */
   private String fullPath;
 
@@ -56,6 +67,11 @@ public class CachedBasicMNode implements ICachedMNode {
     this.cacheMNodeInfo = new CacheMNodeInfo(name);
   }
 
+  @Override
+  public CachedBasicMNode getBasicMNode() {
+    return this;
+  }
+
   @Override
   public String getName() {
     return cacheMNodeInfo.getName();
@@ -99,6 +115,26 @@ public class CachedBasicMNode implements ICachedMNode {
     this.fullPath = fullPath;
   }
 
+  @Override
+  public boolean hasDeviceDescendant() {
+    return hasDeviceDescendant;
+  }
+
+  @Override
+  public void setHasDeviceDescendant(final boolean hasDeviceDescendant) {
+    this.hasDeviceDescendant = hasDeviceDescendant;
+  }
+
+  @Override
+  public boolean isDeviceDescendantComputed() {
+    return deviceDescendantComputed;
+  }
+
+  @Override
+  public void setDeviceDescendantComputed(final boolean 
deviceDescendantComputed) {
+    this.deviceDescendantComputed = deviceDescendantComputed;
+  }
+
   @Override
   public PartialPath getPartialPath() {
     List<String> detachedPath = new ArrayList<>();
@@ -245,6 +281,8 @@ public class CachedBasicMNode implements ICachedMNode {
    *         <li>basicMNodeInfo reference, 8B
    *         <li>parent reference, 8B
    *         <li>fullPath reference, 8B
+   *         <li>hasDeviceDescendant, 1B
+   *         <li>deviceDescendantComputed, 1B
    *       </ol>
    *   <li>MapEntry in parent
    *       <ol>
@@ -256,7 +294,7 @@ public class CachedBasicMNode implements ICachedMNode {
    */
   @Override
   public int estimateSize() {
-    return 8 + 8 + 8 + 8 + 8 + 8 + 28 + cacheMNodeInfo.estimateSize();
+    return 8 + 8 + 8 + 8 + 1 + 1 + 8 + 8 + 28 + cacheMNodeInfo.estimateSize();
   }
 
   @Override
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 f1ac33d99ed..6c9e23e9597 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
@@ -25,15 +25,18 @@ import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.path.PathPatternTree;
 import org.apache.iotdb.commons.path.fa.IFAState;
 import org.apache.iotdb.commons.path.fa.IFATransition;
+import org.apache.iotdb.commons.path.fa.match.IStateMatchInfo;
 import org.apache.iotdb.commons.schema.node.IMNode;
 import org.apache.iotdb.commons.schema.node.role.IDeviceMNode;
 import org.apache.iotdb.commons.schema.node.utils.IMNodeFactory;
 import org.apache.iotdb.commons.schema.node.utils.IMNodeIterator;
 import org.apache.iotdb.commons.schema.tree.AbstractTreeVisitor;
 import org.apache.iotdb.db.schemaengine.schemaregion.mtree.IMTreeStore;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.IMemMNode;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.mem.mnode.iterator.MNodeIterator;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.ReentrantReadOnlyCachedMTreeStore;
 import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.memory.ReleaseFlushMonitor;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.mnode.ICachedMNode;
 import org.apache.iotdb.db.schemaengine.schemaregion.utils.MNodeUtils;
 import org.apache.iotdb.db.schemaengine.template.Template;
 
@@ -41,9 +44,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Set;
 
 import static org.apache.iotdb.commons.conf.IoTDBConstant.PATH_ROOT;
 import static org.apache.iotdb.commons.schema.SchemaConstant.NON_TEMPLATE;
@@ -248,10 +253,107 @@ public abstract class Traverser<R, N extends IMNode<N>> 
extends AbstractTreeVisi
     if (parent.isAboveDatabase()) {
       return new MNodeIterator<>(parent.getChildren().values().iterator());
     } else {
+      final Iterator<N> optimizedIterator =
+          shouldUseLeafDeviceMeasurementOptimization()
+              ? getOptimizedChildrenIterator(parent)
+              : null;
+      if (optimizedIterator != null) {
+        return optimizedIterator;
+      }
       return store.getTraverserIterator(parent, templateMap, 
skipPreDeletedSchema);
     }
   }
 
+  protected boolean shouldUseLeafDeviceMeasurementOptimization() {
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Iterator<N> getOptimizedChildrenIterator(final N parent) throws 
MetadataException {
+    if (!parent.isDevice()) {
+      return null;
+    }
+
+    if (parent instanceof IMemMNode) {
+      if (((IMemMNode) parent).hasDeviceDescendant()) {
+        return null;
+      }
+    } else if (parent instanceof ICachedMNode) {
+      if (hasDeviceDescendant((ICachedMNode) parent)) {
+        return null;
+      }
+    } else {
+      return null;
+    }
+
+    final IStateMatchInfo stateMatchInfo = getCurrentStateMatchInfo();
+    final Set<String> candidateNames = new LinkedHashSet<>();
+
+    for (int i = 0; i < stateMatchInfo.getMatchedStateSize(); i++) {
+      final IFAState matchedState = stateMatchInfo.getMatchedState(i);
+
+      for (final IFATransition transition :
+          patternFA.getPreciseMatchTransition(matchedState).values()) {
+        if (patternFA.getNextState(matchedState, transition).isFinal()) {
+          candidateNames.add(transition.getAcceptEvent());
+        }
+      }
+
+      final Iterator<IFATransition> fuzzyTransitionIterator =
+          patternFA.getFuzzyMatchTransitionIterator(matchedState);
+      while (fuzzyTransitionIterator.hasNext()) {
+        if (patternFA.getNextState(matchedState, 
fuzzyTransitionIterator.next()).isFinal()) {
+          return null;
+        }
+      }
+    }
+
+    if (candidateNames.isEmpty()) {
+      return null;
+    }
+
+    // For leaf devices, `**.measurement` does not need to enumerate every 
measurement child.
+    try {
+      return getChildrenIterator(parent, candidateNames.iterator());
+    } catch (final MetadataException e) {
+      throw e;
+    } catch (final Exception e) {
+      throw new MetadataException(e.getMessage(), e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private boolean hasDeviceDescendant(final ICachedMNode node) throws 
MetadataException {
+    if (node.isMeasurement()) {
+      node.setHasDeviceDescendant(false);
+      node.setDeviceDescendantComputed(true);
+      return false;
+    }
+    if (!node.isDeviceDescendantComputed()) {
+      node.setHasDeviceDescendant(hasDeviceDescendantInChildren(node));
+      node.setDeviceDescendantComputed(true);
+    }
+    return node.hasDeviceDescendant();
+  }
+
+  @SuppressWarnings("unchecked")
+  private boolean hasDeviceDescendantInChildren(final ICachedMNode node) 
throws MetadataException {
+    final IMTreeStore<ICachedMNode> cachedStore = (IMTreeStore<ICachedMNode>) 
store;
+    try (final IMNodeIterator<ICachedMNode> iterator = 
cachedStore.getChildrenIterator(node)) {
+      while (iterator.hasNext()) {
+        final ICachedMNode child = iterator.next();
+        try {
+          if (child.isDevice() || hasDeviceDescendant(child)) {
+            return true;
+          }
+        } finally {
+          cachedStore.unPin(child);
+        }
+      }
+      return false;
+    }
+  }
+
   @Override
   protected void releaseNodeIterator(Iterator<N> nodeIterator) {
     if (nodeIterator instanceof IMNodeIterator) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/basic/MeasurementTraverser.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/basic/MeasurementTraverser.java
index 4996e3f0c8b..490145d5371 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/basic/MeasurementTraverser.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/traverser/basic/MeasurementTraverser.java
@@ -84,4 +84,9 @@ public abstract class MeasurementTraverser<R, N extends 
IMNode<N>> extends Trave
   protected boolean shouldVisitSubtreeOfInternalMatchedNode(N node) {
     return !node.isMeasurement();
   }
+
+  @Override
+  protected boolean shouldUseLeafDeviceMeasurementOptimization() {
+    return true;
+  }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionBasicTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionBasicTest.java
index 667ec4bc9b4..913b5b151e8 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionBasicTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionBasicTest.java
@@ -1185,6 +1185,28 @@ public class SchemaRegionBasicTest extends 
AbstractSchemaRegionTest {
     Assert.assertEquals(expectedPathList, actualPathList);
   }
 
+  @Test
+  public void testShowTimeseriesWildcardSuffixWithNestedAndLeafDevices() 
throws Exception {
+    final ISchemaRegion schemaRegion = getSchemaRegion("root.test", 0);
+
+    SchemaRegionTestUtil.createSimpleTimeSeriesByList(
+        schemaRegion,
+        Arrays.asList(
+            "root.test.d1.s1", "root.test.d1.a.d2.s1", "root.test.d3.s1", 
"root.test.d4.s2"));
+
+    final List<ITimeSeriesSchemaInfo> result =
+        SchemaRegionTestUtil.showTimeseries(schemaRegion, new 
PartialPath("root.test.**.s1"));
+    final Set<String> expectedPathList =
+        new HashSet<>(Arrays.asList("root.test.d1.s1", "root.test.d1.a.d2.s1", 
"root.test.d3.s1"));
+    Assert.assertEquals(expectedPathList.size(), result.size());
+
+    final Set<String> actualPathList = new HashSet<>();
+    for (final ITimeSeriesSchemaInfo timeSeriesSchemaInfo : result) {
+      actualPathList.add(timeSeriesSchemaInfo.getFullPath());
+    }
+    Assert.assertEquals(expectedPathList, actualPathList);
+  }
+
   @Test
   public void testGetMatchedDevicesWithSpecialPattern2() throws Exception {
     final ISchemaRegion schemaRegion = getSchemaRegion("root.test", 0);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
new file mode 100644
index 00000000000..1dfa4a056bb
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.mtree.impl.mem;
+
+import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.path.MeasurementPath;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.SchemaConstant;
+import org.apache.iotdb.commons.schema.node.role.IMeasurementMNode;
+import org.apache.iotdb.commons.schema.node.utils.IMNodeFactory;
+import org.apache.iotdb.commons.schema.node.utils.IMNodeIterator;
+import org.apache.iotdb.db.exception.metadata.PathNotExistException;
+import org.apache.iotdb.db.schemaengine.metric.SchemaRegionMemMetric;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaEngineStatistics;
+import org.apache.iotdb.db.schemaengine.rescon.MemSchemaRegionStatistics;
+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.schemaengine.schemaregion.mtree.traverser.collector.MeasurementCollector;
+import org.apache.iotdb.db.schemaengine.template.Template;
+
+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.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class MTreeBelowSGMemoryImplTest {
+
+  @Test
+  public void 
testDeviceDescendantFlagIsMaintainedAcrossCreateDeleteAndRebuild() throws 
Exception {
+    final MTreeBelowSGMemoryImpl mtree = newMTree();
+
+    mtree.createTimeSeries(
+        new MeasurementPath("root.sg.a.b.s1"),
+        TSDataType.BOOLEAN,
+        TSEncoding.PLAIN,
+        CompressionType.SNAPPY,
+        null,
+        null,
+        false,
+        null);
+
+    IMemMNode database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    IMemMNode aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    IMemMNode bNode = mtree.getNodeByPath(new PartialPath("root.sg.a.b"));
+
+    Assert.assertTrue(database.hasDeviceDescendant());
+    Assert.assertFalse(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(bNode.isDevice());
+    Assert.assertFalse(bNode.hasDeviceDescendant());
+
+    mtree.createTimeSeries(
+        new MeasurementPath("root.sg.a.s0"),
+        TSDataType.BOOLEAN,
+        TSEncoding.PLAIN,
+        CompressionType.SNAPPY,
+        null,
+        null,
+        false,
+        null);
+    mtree.createTimeSeries(
+        new MeasurementPath("root.sg.c.d.s2"),
+        TSDataType.BOOLEAN,
+        TSEncoding.PLAIN,
+        CompressionType.SNAPPY,
+        null,
+        null,
+        false,
+        null);
+
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    IMemMNode cNode = mtree.getNodeByPath(new PartialPath("root.sg.c"));
+    Assert.assertTrue(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(cNode.hasDeviceDescendant());
+
+    database.setHasDeviceDescendant(false);
+    aNode.setHasDeviceDescendant(false);
+    bNode.setHasDeviceDescendant(true);
+    cNode.setHasDeviceDescendant(false);
+    mtree.rebuildSubtreeMeasurementCount();
+
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    bNode = mtree.getNodeByPath(new PartialPath("root.sg.a.b"));
+    cNode = mtree.getNodeByPath(new PartialPath("root.sg.c"));
+    Assert.assertTrue(database.hasDeviceDescendant());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertFalse(bNode.hasDeviceDescendant());
+    Assert.assertTrue(cNode.hasDeviceDescendant());
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.a.s0"));
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    Assert.assertFalse(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.a.b.s1"));
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    Assert.assertTrue(database.hasDeviceDescendant());
+    assertPathNotExist(mtree, new PartialPath("root.sg.a"));
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.c.d.s2"));
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    Assert.assertFalse(database.hasDeviceDescendant());
+    assertPathNotExist(mtree, new PartialPath("root.sg.c"));
+  }
+
+  @Test
+  public void testLeafDeviceWildcardSuffixUsesDirectMeasurementLookup() throws 
Exception {
+    final PartialPath databasePath = new PartialPath("root.sg");
+    final MemSchemaRegionStatistics regionStatistics = newRegionStatistics();
+    final CountingMemMTreeStore store =
+        new CountingMemMTreeStore(
+            databasePath, regionStatistics, new 
SchemaRegionMemMetric(regionStatistics, "root.sg"));
+    final IMNodeFactory<IMemMNode> nodeFactory =
+        MNodeFactoryLoader.getInstance().getMemMNodeIMNodeFactory();
+
+    final IMemMNode databaseMNode = store.getRoot();
+    final IMemMNode rootNode = store.generatePrefix(databasePath);
+    final IMemMNode deviceNode =
+        store.addChild(databaseMNode, "d1", 
nodeFactory.createInternalMNode(databaseMNode, "d1"));
+    store.setToEntity(deviceNode);
+
+    for (int i = 0; i < 64; i++) {
+      final String measurement = i == 0 ? "target" : "s" + i;
+      final IMeasurementMNode<IMemMNode> measurementMNode =
+          nodeFactory.createMeasurementMNode(
+              deviceNode.getAsDeviceMNode(),
+              measurement,
+              new MeasurementSchema(
+                  measurement, TSDataType.BOOLEAN, TSEncoding.PLAIN, 
CompressionType.SNAPPY),
+              null);
+      store.addChild(deviceNode, measurement, measurementMNode.getAsMNode());
+    }
+
+    store.watchParent(deviceNode);
+    final List<String> matchedPaths = new ArrayList<>();
+    try (final MeasurementCollector<Void, IMemMNode> collector =
+        new MeasurementCollector<Void, IMemMNode>(
+            rootNode,
+            new PartialPath("root.sg.**.target"),
+            store,
+            false,
+            SchemaConstant.ALL_MATCH_SCOPE) {
+          @Override
+          protected Void collectMeasurement(final IMeasurementMNode<IMemMNode> 
node) {
+            
matchedPaths.add(getCurrentMeasurementPathInTraverse(node).getFullPath());
+            return null;
+          }
+        }) {
+      collector.traverse();
+    }
+
+    Assert.assertEquals(Collections.singletonList("root.sg.d1.target"), 
matchedPaths);
+    Assert.assertEquals(0, store.getWatchedTraverserIteratorCount());
+    Assert.assertEquals(1, store.getWatchedChildLookupCount());
+  }
+
+  private static MTreeBelowSGMemoryImpl newMTree() throws Exception {
+    final MemSchemaRegionStatistics regionStatistics = newRegionStatistics();
+    return new MTreeBelowSGMemoryImpl(
+        new PartialPath("root.sg"),
+        node -> Collections.emptyMap(),
+        node -> Collections.emptyMap(),
+        regionStatistics,
+        new SchemaRegionMemMetric(regionStatistics, "root.sg"));
+  }
+
+  private static MemSchemaRegionStatistics newRegionStatistics() {
+    return new MemSchemaRegionStatistics(0, new MemSchemaEngineStatistics());
+  }
+
+  private static void assertPathNotExist(final MTreeBelowSGMemoryImpl mtree, 
final PartialPath path)
+      throws MetadataException {
+    try {
+      mtree.getNodeByPath(path);
+      Assert.fail("Expected path not exist: " + path.getFullPath());
+    } catch (final PathNotExistException ignored) {
+      // expected
+    }
+  }
+
+  private static class CountingMemMTreeStore extends MemMTreeStore {
+    private IMemMNode watchedParent;
+    private int watchedTraverserIteratorCount;
+    private int watchedChildLookupCount;
+
+    private CountingMemMTreeStore(
+        final PartialPath rootPath,
+        final MemSchemaRegionStatistics regionStatistics,
+        final SchemaRegionMemMetric metric) {
+      super(rootPath, regionStatistics, metric);
+    }
+
+    private void watchParent(final IMemMNode parent) {
+      this.watchedParent = parent;
+      this.watchedTraverserIteratorCount = 0;
+      this.watchedChildLookupCount = 0;
+    }
+
+    private int getWatchedTraverserIteratorCount() {
+      return watchedTraverserIteratorCount;
+    }
+
+    private int getWatchedChildLookupCount() {
+      return watchedChildLookupCount;
+    }
+
+    @Override
+    public IMemMNode getChild(final IMemMNode parent, final String name) {
+      if (parent == watchedParent) {
+        watchedChildLookupCount++;
+      }
+      return super.getChild(parent, name);
+    }
+
+    @Override
+    public IMNodeIterator<IMemMNode> getTraverserIterator(
+        final IMemMNode parent,
+        final Map<Integer, Template> templateMap,
+        final boolean skipPreDeletedSchema)
+        throws MetadataException {
+      if (parent == watchedParent) {
+        watchedTraverserIteratorCount++;
+      }
+      return super.getTraverserIterator(parent, templateMap, 
skipPreDeletedSchema);
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
new file mode 100644
index 00000000000..65a6f31a75d
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.mtree.impl.pbtree;
+
+import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.path.MeasurementPath;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.schema.SchemaConstant;
+import org.apache.iotdb.commons.schema.node.role.IMeasurementMNode;
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.exception.metadata.PathNotExistException;
+import org.apache.iotdb.db.schemaengine.metric.SchemaRegionCachedMetric;
+import org.apache.iotdb.db.schemaengine.rescon.CachedSchemaEngineStatistics;
+import org.apache.iotdb.db.schemaengine.rescon.CachedSchemaRegionStatistics;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.memory.ReleaseFlushMonitor;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.impl.pbtree.mnode.ICachedMNode;
+import 
org.apache.iotdb.db.schemaengine.schemaregion.mtree.traverser.collector.MeasurementCollector;
+
+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.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class MTreeBelowSGCachedImplTest {
+
+  private static final Field STORE_FIELD;
+  private static final Field ROOT_NODE_FIELD;
+
+  static {
+    try {
+      STORE_FIELD = MTreeBelowSGCachedImpl.class.getDeclaredField("store");
+      STORE_FIELD.setAccessible(true);
+      ROOT_NODE_FIELD = 
MTreeBelowSGCachedImpl.class.getDeclaredField("rootNode");
+      ROOT_NODE_FIELD.setAccessible(true);
+    } catch (final NoSuchFieldException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+  private int rawCachedMNodeSize;
+  private MTreeBelowSGCachedImpl mtree;
+
+  @Before
+  public void setUp() {
+    rawCachedMNodeSize = config.getCachedMNodeSizeInPBTreeMode();
+    config.setCachedMNodeSizeInPBTreeMode(10000);
+    ReleaseFlushMonitor.getInstance().clear();
+    ReleaseFlushMonitor.getInstance().init(new CachedSchemaEngineStatistics());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (mtree != null) {
+      mtree.clear();
+      mtree = null;
+    }
+    ReleaseFlushMonitor.getInstance().clear();
+    FileUtils.deleteDirectory(new File(config.getSchemaDir()));
+    config.setCachedMNodeSizeInPBTreeMode(rawCachedMNodeSize);
+  }
+
+  @Test
+  public void 
testDeviceDescendantFlagIsMaintainedAcrossCreateDeleteAndLazyRecompute()
+      throws Exception {
+    mtree = newMTree();
+
+    createBooleanTimeSeries("root.sg.a.b.s1");
+
+    ICachedMNode database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    ICachedMNode aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    ICachedMNode bNode = mtree.getNodeByPath(new PartialPath("root.sg.a.b"));
+
+    Assert.assertTrue(database.hasDeviceDescendant());
+    Assert.assertTrue(database.isDeviceDescendantComputed());
+    Assert.assertFalse(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(aNode.isDeviceDescendantComputed());
+    Assert.assertTrue(bNode.isDevice());
+    Assert.assertFalse(bNode.hasDeviceDescendant());
+    Assert.assertTrue(bNode.isDeviceDescendantComputed());
+    mtree.unPinMNode(database);
+    mtree.unPinMNode(aNode);
+    mtree.unPinMNode(bNode);
+
+    createBooleanTimeSeries("root.sg.a.s0");
+    createBooleanTimeSeries("root.sg.c.d.s2");
+
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    ICachedMNode cNode = mtree.getNodeByPath(new PartialPath("root.sg.c"));
+    Assert.assertTrue(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(aNode.isDeviceDescendantComputed());
+    Assert.assertTrue(cNode.hasDeviceDescendant());
+    Assert.assertTrue(cNode.isDeviceDescendantComputed());
+
+    aNode.setHasDeviceDescendant(false);
+    aNode.setDeviceDescendantComputed(false);
+    mtree.unPinMNode(aNode);
+    cNode.setHasDeviceDescendant(false);
+    cNode.setDeviceDescendantComputed(false);
+    mtree.unPinMNode(cNode);
+
+    Assert.assertEquals(
+        Collections.singletonList("root.sg.a.b.s1"),
+        collectMeasurementPaths(new PartialPath("root.sg.a.**.s1")));
+
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(aNode.isDeviceDescendantComputed());
+    mtree.unPinMNode(aNode);
+    cNode = mtree.getNodeByPath(new PartialPath("root.sg.c"));
+    Assert.assertFalse(cNode.isDeviceDescendantComputed());
+    mtree.unPinMNode(cNode);
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.a.s0"));
+    aNode = mtree.getNodeByPath(new PartialPath("root.sg.a"));
+    Assert.assertFalse(aNode.isDevice());
+    Assert.assertTrue(aNode.hasDeviceDescendant());
+    Assert.assertTrue(aNode.isDeviceDescendantComputed());
+    mtree.unPinMNode(aNode);
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.a.b.s1"));
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    Assert.assertTrue(database.hasDeviceDescendant());
+    Assert.assertTrue(database.isDeviceDescendantComputed());
+    mtree.unPinMNode(database);
+    assertPathNotExist(new PartialPath("root.sg.a"));
+
+    mtree.deleteTimeseries(new PartialPath("root.sg.c.d.s2"));
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    Assert.assertFalse(database.hasDeviceDescendant());
+    Assert.assertTrue(database.isDeviceDescendantComputed());
+    mtree.unPinMNode(database);
+    assertPathNotExist(new PartialPath("root.sg.c"));
+  }
+
+  private MTreeBelowSGCachedImpl newMTree() throws Exception {
+    final CachedSchemaRegionStatistics regionStatistics =
+        new CachedSchemaRegionStatistics(0, new 
CachedSchemaEngineStatistics());
+    return new MTreeBelowSGCachedImpl(
+        new PartialPath("root.sg"),
+        node -> Collections.emptyMap(),
+        node -> Collections.emptyMap(),
+        () -> {},
+        node -> {},
+        node -> {},
+        0,
+        regionStatistics,
+        new SchemaRegionCachedMetric(regionStatistics, "root.sg"));
+  }
+
+  private void createBooleanTimeSeries(final String path) throws 
MetadataException {
+    mtree.createTimeSeries(
+        new MeasurementPath(path),
+        TSDataType.BOOLEAN,
+        TSEncoding.PLAIN,
+        CompressionType.SNAPPY,
+        null,
+        null);
+  }
+
+  private List<String> collectMeasurementPaths(final PartialPath pattern) 
throws Exception {
+    final ICachedMNode rootNode = (ICachedMNode) ROOT_NODE_FIELD.get(mtree);
+    final CachedMTreeStore store = (CachedMTreeStore) STORE_FIELD.get(mtree);
+    final List<String> matchedPaths = new ArrayList<>();
+    try (MeasurementCollector<Void, ICachedMNode> collector =
+        new MeasurementCollector<Void, ICachedMNode>(
+            rootNode, pattern, store, false, SchemaConstant.ALL_MATCH_SCOPE) {
+          @Override
+          protected Void collectMeasurement(final 
IMeasurementMNode<ICachedMNode> node) {
+            
matchedPaths.add(getCurrentMeasurementPathInTraverse(node).getFullPath());
+            return null;
+          }
+        }) {
+      collector.traverse();
+    }
+    return matchedPaths;
+  }
+
+  private void assertPathNotExist(final PartialPath path) throws 
MetadataException {
+    try {
+      mtree.getNodeByPath(path);
+      Assert.fail("Expected path not exist: " + path.getFullPath());
+    } catch (final PathNotExistException ignored) {
+      // expected
+    }
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/common/AbstractAboveDatabaseMNode.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/common/AbstractAboveDatabaseMNode.java
index ab0592480e5..fc379e34e5b 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/common/AbstractAboveDatabaseMNode.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/common/AbstractAboveDatabaseMNode.java
@@ -37,6 +37,10 @@ public abstract class AbstractAboveDatabaseMNode<N extends 
IMNode<N>, BasicNode
     this.basicMNode = basicMNode;
   }
 
+  public BasicNode getBasicMNode() {
+    return basicMNode;
+  }
+
   @Override
   public String getName() {
     return basicMNode.getName();
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/utils/IMNodeIterator.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/utils/IMNodeIterator.java
index 75fd9d40c67..d1f410c5460 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/utils/IMNodeIterator.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/node/utils/IMNodeIterator.java
@@ -22,7 +22,7 @@ import org.apache.iotdb.commons.schema.node.IMNode;
 
 import java.util.Iterator;
 
-public interface IMNodeIterator<N extends IMNode<?>> extends Iterator<N> {
+public interface IMNodeIterator<N extends IMNode<?>> extends Iterator<N>, 
AutoCloseable {
 
   void skipTemplateChildren();
 
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 caa8019ef87..28b192e99ca 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
@@ -413,6 +413,10 @@ public abstract class AbstractTreeVisitor<N extends 
ITreeNode, R> implements Sch
     }
   }
 
+  protected final IStateMatchInfo getCurrentStateMatchInfo() {
+    return currentStateMatchInfo;
+  }
+
   // Release a child node.
   protected void releaseNode(N node) {}
 

Reply via email to