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

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

commit a565a06825317e5f39393e619fb44e3fbbb88a9b
Author: Caideyipi <[email protected]>
AuthorDate: Mon Jun 1 10:58:03 2026 +0800

    Added flags to mark whether a device has device descendants to optimize 
query like select xx from xxx.** (#17672)
    
    * flag-
    
    * Fix
    
    * Address device descendant flag review comments
---
 .../mtree/impl/mem/MTreeBelowSGMemoryImpl.java     |  81 ++++--
 .../mtree/impl/mem/mnode/IMemMNode.java            |  23 +-
 .../mtree/impl/mem/mnode/basic/BasicMNode.java     |  21 +-
 .../impl/mem/mnode/impl/AboveDatabaseMNode.java    |  10 -
 .../mtree/impl/mem/mnode/impl/DatabaseMNode.java   |  10 -
 .../impl/mem/mnode/impl/MeasurementMNode.java      |  10 -
 .../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 | 281 +++++++++++++++++++++
 .../impl/pbtree/MTreeBelowSGCachedImplTest.java    | 217 ++++++++++++++++
 .../node/common/AbstractAboveDatabaseMNode.java    |   4 +
 .../commons/schema/node/utils/IMNodeIterator.java  |   2 +-
 .../commons/schema/tree/AbstractTreeVisitor.java   |   4 +
 17 files changed, 869 insertions(+), 65 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 ad1fff7403d..44c4c49f274 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
@@ -208,6 +208,43 @@ public class MTreeBelowSGMemoryImpl {
     }
   }
 
+  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();
@@ -316,7 +353,7 @@ public class MTreeBelowSGMemoryImpl {
       if (device.isDevice()) {
         entityMNode = device.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(device);
+        entityMNode = setToEntityAndUpdateFlags(device);
       }
 
       // create a non-aligned time series
@@ -410,7 +447,7 @@ public class MTreeBelowSGMemoryImpl {
       if (device.isDevice()) {
         entityMNode = device.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(device);
+        entityMNode = setToEntityAndUpdateFlags(device);
         entityMNode.setAligned(true);
       }
 
@@ -658,14 +695,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;
+            }
           }
         }
       }
@@ -674,6 +712,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);
@@ -1034,7 +1073,7 @@ public class MTreeBelowSGMemoryImpl {
       if (cur.isDevice()) {
         entityMNode = cur.getAsDeviceMNode();
       } else {
-        entityMNode = store.setToEntity(cur);
+        entityMNode = setToEntityAndUpdateFlags(cur);
       }
     }
 
@@ -1142,7 +1181,7 @@ public class MTreeBelowSGMemoryImpl {
     if (cur.isDevice()) {
       entityMNode = cur.getAsDeviceMNode();
     } else {
-      entityMNode = store.setToEntity(cur);
+      entityMNode = setToEntityAndUpdateFlags(cur);
     }
 
     if (!entityMNode.isAligned()) {
@@ -1197,14 +1236,21 @@ public class MTreeBelowSGMemoryImpl {
 
   private long rebuildSubtreeMeasurementCountFromNode(final IMemMNode node) {
     long count = node.isMeasurement() ? 1L : 0L;
-    final IMNodeIterator<IMemMNode> iterator = store.getChildrenIterator(node);
-    while (iterator.hasNext()) {
-      count += rebuildSubtreeMeasurementCountFromNode(iterator.next());
+    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;
   }
 
@@ -1787,7 +1833,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);
@@ -1928,7 +1974,7 @@ public class MTreeBelowSGMemoryImpl {
             (TableDeviceInfo<IMemMNode>) entityMNode.getDeviceInfo();
         attributeUpdater.accept(deviceInfo.getAttributePointer());
       } else {
-        entityMNode = store.setToEntity(cur);
+        entityMNode = setToEntityAndUpdateFlags(cur);
         final TableDeviceInfo<IMemMNode> deviceInfo = new TableDeviceInfo<>();
         deviceInfo.setAttributePointer(attributePointerGetter.getAsInt());
         entityMNode.getAsInternalMNode().setDeviceInfo(deviceInfo);
@@ -2046,6 +2092,7 @@ public class MTreeBelowSGMemoryImpl {
       collector.traverse();
     }
     databaseMNode.deleteChild(tableName);
+    refreshAncestorsHavingDeviceDescendant(databaseMNode);
     regionStatistics.resetTableDevice(tableName);
     store.releaseMemory(memoryReleased.get());
     return true;
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 6cd14800f3e..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,14 +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> {
 
+  BasicMNode getBasicMNode();
+
   /**
    * The count of measurement nodes contained in the subtree rooted at this 
node. The counter is
    * maintained in memory only.
    */
-  long getSubtreeMeasurementCount();
+  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();
+  }
 
-  void setSubtreeMeasurementCount(long subtreeMeasurementCount);
+  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 bd80d10193b..fc46f9a1196 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
@@ -50,6 +50,9 @@ public class BasicMNode implements IMemMNode {
   /** 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;
 
@@ -59,6 +62,11 @@ public class BasicMNode implements IMemMNode {
     this.basicMNodeInfo = new BasicMNodeInfo(name);
   }
 
+  @Override
+  public BasicMNode getBasicMNode() {
+    return this;
+  }
+
   @Override
   public String getName() {
     return basicMNodeInfo.getName();
@@ -113,6 +121,16 @@ public class BasicMNode implements IMemMNode {
     this.subtreeMeasurementCount = subtreeMeasurementCount;
   }
 
+  @Override
+  public boolean hasDeviceDescendant() {
+    return hasDeviceDescendant;
+  }
+
+  @Override
+  public void setHasDeviceDescendant(final boolean hasDeviceDescendant) {
+    this.hasDeviceDescendant = hasDeviceDescendant;
+  }
+
   @Override
   public PartialPath getPartialPath() {
     final List<String> detachedPath = new ArrayList<>();
@@ -240,6 +258,7 @@ public class BasicMNode implements IMemMNode {
    *         <li>parent reference, 8B
    *         <li>fullPath reference, 8B
    *         <li>subtreeMeasurementCount, 8B
+   *         <li>hasDeviceDescendant, 1B
    *       </ol>
    *   <li>MapEntry in parent
    *       <ol>
@@ -251,7 +270,7 @@ public class BasicMNode implements IMemMNode {
    */
   @Override
   public int estimateSize() {
-    return 8 + 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/mem/mnode/impl/AboveDatabaseMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/AboveDatabaseMNode.java
index 87144d4954a..cff30d8b8c4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/AboveDatabaseMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/AboveDatabaseMNode.java
@@ -33,14 +33,4 @@ public class AboveDatabaseMNode extends 
AbstractAboveDatabaseMNode<IMemMNode, Ba
   public IMemMNode getAsMNode() {
     return this;
   }
-
-  @Override
-  public long getSubtreeMeasurementCount() {
-    return basicMNode.getSubtreeMeasurementCount();
-  }
-
-  @Override
-  public void setSubtreeMeasurementCount(long subtreeMeasurementCount) {
-    basicMNode.setSubtreeMeasurementCount(subtreeMeasurementCount);
-  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/DatabaseMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/DatabaseMNode.java
index 290cf427360..c6b2f5e2427 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/DatabaseMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/DatabaseMNode.java
@@ -45,14 +45,4 @@ public class DatabaseMNode extends 
AbstractDatabaseMNode<IMemMNode, BasicInterna
   public void setDeviceInfo(IDeviceInfo<IMemMNode> deviceInfo) {
     basicMNode.setDeviceInfo(deviceInfo);
   }
-
-  @Override
-  public long getSubtreeMeasurementCount() {
-    return basicMNode.getSubtreeMeasurementCount();
-  }
-
-  @Override
-  public void setSubtreeMeasurementCount(long subtreeMeasurementCount) {
-    basicMNode.setSubtreeMeasurementCount(subtreeMeasurementCount);
-  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/MeasurementMNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/MeasurementMNode.java
index d2a2cbd80c9..a40cbe6bc0f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/MeasurementMNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/mnode/impl/MeasurementMNode.java
@@ -52,14 +52,4 @@ public class MeasurementMNode extends 
AbstractMeasurementMNode<IMemMNode, BasicM
   public final boolean isLogicalView() {
     return false;
   }
-
-  @Override
-  public long getSubtreeMeasurementCount() {
-    return basicMNode.getSubtreeMeasurementCount();
-  }
-
-  @Override
-  public void setSubtreeMeasurementCount(long subtreeMeasurementCount) {
-    basicMNode.setSubtreeMeasurementCount(subtreeMeasurementCount);
-  }
 }
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 edf4c17cb45..a660314df95 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
@@ -138,6 +138,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,
@@ -354,7 +416,7 @@ public class MTreeBelowSGCachedImpl {
           if (device.isDevice()) {
             entityMNode = device.getAsDeviceMNode();
           } else {
-            entityMNode = store.setToEntity(device);
+            entityMNode = setToEntityAndUpdateFlags(device);
             device = entityMNode.getAsMNode();
           }
 
@@ -459,7 +521,7 @@ public class MTreeBelowSGCachedImpl {
           if (device.isDevice()) {
             entityMNode = device.getAsDeviceMNode();
           } else {
-            entityMNode = store.setToEntity(device);
+            entityMNode = setToEntityAndUpdateFlags(device);
             entityMNode.setAligned(true);
             device = entityMNode.getAsMNode();
           }
@@ -685,8 +747,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);
@@ -698,12 +759,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));
@@ -730,14 +790,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();
     }
   }
 
@@ -1076,7 +1133,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.
@@ -1198,7 +1255,7 @@ public class MTreeBelowSGCachedImpl {
         if (cur.isDevice()) {
           entityMNode = cur.getAsDeviceMNode();
         } else {
-          entityMNode = store.setToEntity(cur);
+          entityMNode = setToEntityAndUpdateFlags(cur);
         }
 
         if (entityMNode.isUseTemplate()) {
@@ -1244,7 +1301,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 6f29c738a04..c23eebbb272 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
@@ -48,6 +48,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;
 
@@ -57,6 +68,11 @@ public class CachedBasicMNode implements ICachedMNode {
     this.cacheMNodeInfo = new CacheMNodeInfo(name);
   }
 
+  @Override
+  public CachedBasicMNode getBasicMNode() {
+    return this;
+  }
+
   @Override
   public String getName() {
     return cacheMNodeInfo.getName();
@@ -100,6 +116,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<>();
@@ -246,6 +282,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>
@@ -257,7 +295,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 24b7e93939e..93c0d9e31c7 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,6 +25,7 @@ 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;
@@ -32,18 +33,22 @@ import 
org.apache.iotdb.commons.schema.node.utils.IMNodeIterator;
 import org.apache.iotdb.commons.schema.template.Template;
 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.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.schema.SchemaConstant.NON_TEMPLATE;
 
@@ -247,10 +252,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 601233d9d60..3de059127d3 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..1dd3fa456b7
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.commons.schema.template.Template;
+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.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;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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 testDeviceDescendantFlagIsMaintainedAcrossTableDeviceDrop() 
throws Exception {
+    final MTreeBelowSGMemoryImpl mtree = newMTree();
+    final AtomicInteger attributePointer = new AtomicInteger();
+
+    mtree.createOrUpdateTableDevice(
+        "t",
+        new String[] {"hebei", "p_1", "d_0"},
+        attributePointer::getAndIncrement,
+        ignored -> {});
+
+    IMemMNode database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    final IMemMNode table = mtree.getNodeByPath(new PartialPath("root.sg.t"));
+    final IMemMNode province = mtree.getNodeByPath(new 
PartialPath("root.sg.t.hebei"));
+    final IMemMNode device = mtree.getNodeByPath(new 
PartialPath("root.sg.t.hebei.p_1.d_0"));
+    Assert.assertTrue(database.hasDeviceDescendant());
+    Assert.assertTrue(table.hasDeviceDescendant());
+    Assert.assertTrue(province.hasDeviceDescendant());
+    Assert.assertTrue(device.isDevice());
+    Assert.assertFalse(device.hasDeviceDescendant());
+
+    Assert.assertTrue(mtree.deleteTableDevice("t", ignored -> {}));
+    database = mtree.getNodeByPath(new PartialPath("root.sg"));
+    Assert.assertFalse(database.hasDeviceDescendant());
+    assertPathNotExist(mtree, new PartialPath("root.sg.t"));
+  }
+
+  @Test
+  public void testLeafDeviceWildcardSuffixUsesDirectMeasurementLookup() throws 
Exception {
+    final PartialPath databasePath = 
PartialPath.getQualifiedDatabasePartialPath("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(
+        PartialPath.getQualifiedDatabasePartialPath("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..81936640b49
--- /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.tsfile.enums.TSDataType;
+import org.apache.tsfile.external.commons.io.FileUtils;
+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(
+        PartialPath.getQualifiedDatabasePartialPath("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 3b821567c38..757e6bafbbc 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
@@ -38,6 +38,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 a60a4ff121a..512270c379a 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
@@ -415,6 +415,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