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

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


The following commit(s) were added to refs/heads/master by this push:
     new e75d1466251 Fix retry bug in tree model query which ignore the time 
filter (#18179)
e75d1466251 is described below

commit e75d1466251111f5a8b38cfc49643ef50d447749
Author: Jackie Tien <[email protected]>
AuthorDate: Tue Jul 14 14:42:29 2026 +0800

    Fix retry bug in tree model query which ignore the time filter (#18179)
---
 .../queryengine/plan/analyze/AnalyzeVisitor.java   | 106 +++++----
 .../db/queryengine/plan/analyze/Analyzer.java      |  16 +-
 .../plan/analyze/ConcatPathRewriter.java           |  23 +-
 .../queryengine/plan/analyze/PredicateUtils.java   | 177 ++++++++------
 .../plan/analyze/TemplatedAggregationAnalyze.java  |  71 ++++--
 .../queryengine/plan/analyze/TemplatedAnalyze.java |  20 +-
 .../plan/analyze/TreeAnalysisMutationJournal.java  | 255 +++++++++++++++++++++
 .../queryengine/plan/execution/QueryExecution.java |  47 +++-
 .../visitor/predicate/PredicateSimplifier.java     |  83 ++++---
 .../db/queryengine/plan/planner/IPlanner.java      |   6 +
 .../plan/planner/LogicalPlanBuilder.java           |  16 +-
 .../queryengine/plan/planner/TreeModelPlanner.java |  20 +-
 .../relational/analyzer/StatementAnalyzer.java     |  21 +-
 .../sql/ast/AbstractQueryDeviceWithCache.java      |  24 ++
 .../plan/relational/sql/ast/CountDevice.java       |   9 +
 .../plan/relational/sql/ast/ShowDevice.java        |  11 +
 .../plan/statement/component/GroupByComponent.java |   5 +
 .../plan/statement/component/OrderByComponent.java |  30 +++
 .../plan/statement/component/WhereCondition.java   |   7 +
 .../db/queryengine/plan/analyze/AnalyzeTest.java   | 238 +++++++++++++++++++
 .../plan/analyze/PredicateUtilsTest.java           | 172 ++++++++++++++
 .../plan/analyze/TemplatedAnalyzeRetryTest.java    | 127 ++++++++++
 .../plan/execution/QueryExecutionRetryTest.java    | 218 ++++++++++++++++++
 .../plan/planner/LogicalPlanBuilderTest.java       | 113 +++++++++
 .../plan/relational/analyzer/AnalyzerTest.java     | 117 ++++++++++
 .../plan/statement/QueryStatementTest.java         |  26 +++
 26 files changed, 1771 insertions(+), 187 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
index d8f1781bdaf..64df0f56e5c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
@@ -232,11 +232,16 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
   private final IPartitionFetcher partitionFetcher;
   private final ISchemaFetcher schemaFetcher;
   private final IModelFetcher modelFetcher;
+  private final TreeAnalysisMutationJournal mutationJournal;
 
-  public AnalyzeVisitor(IPartitionFetcher partitionFetcher, ISchemaFetcher 
schemaFetcher) {
+  public AnalyzeVisitor(
+      IPartitionFetcher partitionFetcher,
+      ISchemaFetcher schemaFetcher,
+      TreeAnalysisMutationJournal mutationJournal) {
     this.partitionFetcher = partitionFetcher;
     this.schemaFetcher = schemaFetcher;
     this.modelFetcher = ModelFetcher.getInstance();
+    this.mutationJournal = mutationJournal;
   }
 
   @Override
@@ -280,7 +285,18 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
 
     try {
       // check for semantic errors
-      queryStatement.semanticCheck();
+      boolean originalCountTimeAggregation = 
queryStatement.isCountTimeAggregation();
+      try {
+        queryStatement.semanticCheck();
+      } finally {
+        // semanticCheck derives this flag from SELECT and writes it into 
QueryStatement. Record the
+        // rare actual change so a failed attempt or dispatch retry restores 
the parser-owned state;
+        // ordinary queries pay no journal entry or copy cost.
+        if (queryStatement.isCountTimeAggregation() != 
originalCountTimeAggregation) {
+          mutationJournal.recordCountTimeAggregationChange(
+              queryStatement, originalCountTimeAggregation);
+        }
+      }
 
       context.initResultSetColumnMemoryTracking(
           queryStatement.getSeriesLimit(),
@@ -311,7 +327,13 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
 
         if (deviceList.size() > 1
             && TemplatedAnalyze.canBuildPlanUseTemplate(
-                analysis, queryStatement, partitionFetcher, schemaTree, 
context, deviceList)) {
+                analysis,
+                queryStatement,
+                partitionFetcher,
+                schemaTree,
+                context,
+                deviceList,
+                mutationJournal)) {
           // when device size is less than 1, there is no need to use template 
optimization, i.e. no
           // need to extract common variables
           return analysis;
@@ -319,6 +341,7 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
 
         if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
           // remove the device which won't appear in resultSet after 
limit/offset
+          mutationJournal.recordLimitOffsetPushDown(queryStatement);
           deviceList =
               pushDownLimitOffsetInGroupByTimeForDevice(
                   deviceList, queryStatement, context.getZoneId());
@@ -460,7 +483,10 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     queryStatement =
         (QueryStatement)
             concatPathRewriter.rewrite(
-                queryStatement, new 
PathPatternTree(queryStatement.useWildcard()), context);
+                queryStatement,
+                new PathPatternTree(queryStatement.useWildcard()),
+                context,
+                mutationJournal);
     analysis.setRealStatement(queryStatement);
 
     // request schema fetch API
@@ -521,24 +547,26 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     Expression globalTimePredicate = null;
     boolean hasValueFilter = false;
     if (queryStatement.getWhereCondition() != null) {
-      WhereCondition whereCondition = queryStatement.getWhereCondition();
-      Expression predicate = whereCondition.getPredicate();
-
-      Pair<Expression, Boolean> resultPair =
-          PredicateUtils.extractGlobalTimePredicate(predicate, true, true);
-      globalTimePredicate = resultPair.left;
+      Expression predicate = queryStatement.getWhereCondition().getPredicate();
+      PredicateUtils.GlobalTimePredicateExtractionResult extractionResult =
+          PredicateUtils.extractGlobalTimePredicate(predicate);
+      globalTimePredicate = extractionResult.getGlobalTimePredicate();
       if (globalTimePredicate != null) {
         globalTimePredicate = 
PredicateUtils.predicateRemoveNot(globalTimePredicate);
       }
-      hasValueFilter = resultPair.right;
-
-      predicate = PredicateUtils.simplifyPredicate(predicate);
-
-      // set where condition to null if predicate is true or time filter.
-      if (!hasValueFilter || predicate.equals(ConstantOperand.TRUE)) {
-        queryStatement.setWhereCondition(null);
-      } else {
-        whereCondition.setPredicate(predicate);
+      hasValueFilter = extractionResult.hasValueFilter();
+
+      Expression residualPredicate =
+          
PredicateUtils.simplifyPredicate(extractionResult.getResidualPredicate());
+
+      // Keep the parser-owned predicate untouched. The journal only saves its 
wrapper reference,
+      // while the analyzer installs a residual working predicate built by 
path-copying the nodes
+      // changed by time-filter extraction and simplification.
+      if (!hasValueFilter || residualPredicate.equals(ConstantOperand.TRUE)) {
+        mutationJournal.replaceWhereCondition(queryStatement, null);
+      } else if (residualPredicate != predicate) {
+        mutationJournal.replaceWhereCondition(
+            queryStatement, 
WhereCondition.fromNormalizedPredicate(residualPredicate));
       }
     }
     analysis.setGlobalTimePredicate(globalTimePredicate);
@@ -1810,7 +1838,7 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
       orderByExpressions.add(orderByExpression);
     }
     analysis.setOrderByExpressions(orderByExpressions);
-    queryStatement.updateSortItems(orderByExpressions);
+    mutationJournal.updateSortItems(queryStatement, orderByExpressions);
   }
 
   private void analyzeOrderBy(
@@ -1986,7 +2014,7 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     }
 
     analysis.setOrderByExpressions(deviceViewOrderByExpression);
-    queryStatement.updateSortItems(deviceViewOrderByExpression);
+    mutationJournal.updateSortItems(queryStatement, 
deviceViewOrderByExpression);
     analysis.setDeviceToSortItems(deviceToSortItems);
     analysis.setDeviceToOrderByExpressions(deviceToOrderByExpressions);
   }
@@ -2315,7 +2343,7 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     if (!queryStatement.isSelectInto()) {
       return;
     }
-    queryStatement.setOrderByComponent(null);
+    mutationJournal.clearOrderByComponent(queryStatement);
 
     List<PartialPath> sourceDevices = new ArrayList<>(deviceSet);
     List<Expression> sourceColumns =
@@ -2373,7 +2401,7 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     if (!queryStatement.isSelectInto()) {
       return;
     }
-    queryStatement.setOrderByComponent(null);
+    mutationJournal.clearOrderByComponent(queryStatement);
 
     List<Expression> sourceColumns =
         outputExpressions.stream()
@@ -3267,19 +3295,19 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
   private void analyzeGlobalTimeConditionInShowMetaData(
       WhereCondition timeCondition, Analysis analysis) {
     Expression predicate = timeCondition.getPredicate();
-    Pair<Expression, Boolean> resultPair =
-        PredicateUtils.extractGlobalTimePredicate(predicate, true, true);
-    if (resultPair.right) {
+    PredicateUtils.GlobalTimePredicateExtractionResult extractionResult =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+    if (extractionResult.hasValueFilter()) {
       throw new SemanticException(
           DataNodeQueryMessages
               
.VALUE_FILTER_CAN_T_EXIST_IN_THE_CONDITION_OF_SHOW_COUNT_CLAUSE_ONLY_TIME_CONDITION);
     }
-    if (resultPair.left == null) {
+    if (extractionResult.getGlobalTimePredicate() == null) {
       throw new SemanticException(
           DataNodeQueryMessages
               
.TIME_CONDITION_CAN_T_BE_EMPTY_IN_THE_CONDITION_OF_SHOW_COUNT_CLAUSE);
     }
-    Expression globalTimePredicate = resultPair.left;
+    Expression globalTimePredicate = extractionResult.getGlobalTimePredicate();
     globalTimePredicate = 
PredicateUtils.predicateRemoveNot(globalTimePredicate);
     analysis.setGlobalTimePredicate(globalTimePredicate);
   }
@@ -3859,12 +3887,10 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     analysis.setSourceExpressions(sourceExpressions);
     sourceExpressions.forEach(expression -> analyzeExpressionType(analysis, 
expression));
 
-    if (!analyzeWhereForShowStatements(
+    analyzeWhereForShowStatements(
         analysis,
         showQueriesStatement.getWhereCondition(),
-        ColumnHeaderConstant.showQueriesColumnHeaders)) {
-      showQueriesStatement.setWhereCondition(null);
-    }
+        ColumnHeaderConstant.showQueriesColumnHeaders);
 
     analysis.setMergeOrderParameter(new 
OrderByParameter(showQueriesStatement.getSortItemList()));
 
@@ -3894,12 +3920,10 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     analysis.setSourceExpressions(sourceExpressions);
     sourceExpressions.forEach(expression -> analyzeExpressionType(analysis, 
expression));
 
-    if (!analyzeWhereForShowStatements(
+    analyzeWhereForShowStatements(
         analysis,
         showDiskUsageStatement.getWhereCondition(),
-        ColumnHeaderConstant.showDiskUsageColumnHeaders)) {
-      showDiskUsageStatement.setWhereCondition(null);
-    }
+        ColumnHeaderConstant.showDiskUsageColumnHeaders);
 
     analysis.setMergeOrderParameter(new 
OrderByParameter(showDiskUsageStatement.getSortItemList()));
 
@@ -3927,17 +3951,15 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
     Expression predicate =
         ExpressionAnalyzer.bindTypeForTimeSeriesOperand(
             whereCondition.getPredicate(), statementColumnHeaders);
-    Pair<Expression, Boolean> resultPair =
-        PredicateUtils.extractGlobalTimePredicate(predicate, true, true);
-    boolean hasValueFilter = resultPair.getRight();
+    PredicateUtils.GlobalTimePredicateExtractionResult extractionResult =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+    boolean hasValueFilter = extractionResult.hasValueFilter();
 
-    predicate = PredicateUtils.simplifyPredicate(predicate);
+    predicate = 
PredicateUtils.simplifyPredicate(extractionResult.getResidualPredicate());
 
     // set where condition to null if predicate is true or don't have value 
filter
     if (!hasValueFilter || predicate.equals(ConstantOperand.TRUE)) {
       return false;
-    } else {
-      whereCondition.setPredicate(predicate);
     }
     TSDataType outputType = analyzeExpressionType(analysis, predicate);
     if (outputType != TSDataType.BOOLEAN) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
index b3517a6086f..4ef11925f26 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
@@ -33,17 +33,28 @@ public class Analyzer {
 
   private final IPartitionFetcher partitionFetcher;
   private final ISchemaFetcher schemaFetcher;
+  private final TreeAnalysisMutationJournal mutationJournal;
 
   public Analyzer(
       MPPQueryContext context, IPartitionFetcher partitionFetcher, 
ISchemaFetcher schemaFetcher) {
+    this(context, partitionFetcher, schemaFetcher, new 
TreeAnalysisMutationJournal());
+  }
+
+  public Analyzer(
+      MPPQueryContext context,
+      IPartitionFetcher partitionFetcher,
+      ISchemaFetcher schemaFetcher,
+      TreeAnalysisMutationJournal mutationJournal) {
     this.context = context;
     this.partitionFetcher = partitionFetcher;
     this.schemaFetcher = schemaFetcher;
+    this.mutationJournal = mutationJournal;
   }
 
   public Analysis analyze(Statement statement) {
     long startTime = System.nanoTime();
-    AnalyzeVisitor visitor = new AnalyzeVisitor(partitionFetcher, 
schemaFetcher);
+    mutationJournal.prepareForAnalyze();
+    AnalyzeVisitor visitor = new AnalyzeVisitor(partitionFetcher, 
schemaFetcher, mutationJournal);
     Analysis analysis = null;
     context.setReserveMemoryForSchemaTreeFunc(
         mem -> {
@@ -53,6 +64,9 @@ public class Analyzer {
         });
     try {
       analysis = visitor.process(statement, context);
+    } catch (RuntimeException | Error e) {
+      mutationJournal.rollback();
+      throw e;
     } finally {
       if (analysis != null && context.releaseSchemaTreeAfterAnalyzing()) {
         analysis.setSchemaTree(null);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
index d7c3505c678..a072bbcd8ec 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
@@ -51,7 +51,10 @@ public class ConcatPathRewriter {
   }
 
   public Statement rewrite(
-      Statement statement, PathPatternTree patternTree, MPPQueryContext 
queryContext)
+      Statement statement,
+      PathPatternTree patternTree,
+      MPPQueryContext queryContext,
+      TreeAnalysisMutationJournal mutationJournal)
       throws StatementAnalyzeException {
     QueryStatement queryStatement = (QueryStatement) statement;
     this.patternTree = patternTree;
@@ -79,20 +82,20 @@ public class ConcatPathRewriter {
       // concat SELECT with FROM
       List<ResultColumn> resultColumns =
           concatSelectWithFrom(queryStatement.getSelectComponent(), 
prefixPaths, queryContext);
-      queryStatement.getSelectComponent().setResultColumns(resultColumns);
+      mutationJournal.replaceResultColumns(queryStatement, resultColumns);
 
       // concat GROUP BY with FROM
       if (queryStatement.hasGroupByExpression()) {
-        queryStatement
-            .getGroupByComponent()
-            .setControlColumnExpression(
-                contactGroupByWithFrom(
-                    
queryStatement.getGroupByComponent().getControlColumnExpression(),
-                    prefixPaths,
-                    queryContext));
+        mutationJournal.replaceGroupByControlExpression(
+            queryStatement,
+            contactGroupByWithFrom(
+                
queryStatement.getGroupByComponent().getControlColumnExpression(),
+                prefixPaths,
+                queryContext));
       }
       if (queryStatement.hasOrderByExpression()) {
-        List<Expression> sortItemExpressions = 
queryStatement.getExpressionSortItemList();
+        List<Expression> sortItemExpressions =
+            
mutationJournal.getMutableOrderByComponent(queryStatement).getExpressionSortItemList();
         sortItemExpressions.replaceAll(
             expression -> contactOrderByWithFrom(expression, prefixPaths, 
queryContext));
       }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
index fc5e9329f8a..edf52063588 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
@@ -47,7 +47,6 @@ import 
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate.Reverse
 
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.read.filter.basic.Filter;
-import org.apache.tsfile.utils.Pair;
 
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -66,88 +65,85 @@ public class PredicateUtils {
   }
 
   /**
-   * Extract global time predicate from query predicate.
+   * Extracts the global time predicate and the predicate that still needs to 
be evaluated.
+   *
+   * <p>This method never modifies {@code predicate}. A {@code null} global 
time predicate
+   * represents {@code TRUE}. The returned predicates satisfy the following 
invariant:
+   *
+   * <pre>
+   * predicate == globalTimePredicate AND residualPredicate
+   * </pre>
+   *
+   * where a {@code null} global time predicate is omitted from the 
conjunction.
    *
    * @param predicate raw query predicate
-   * @param canRewrite determined by the father of current expression
-   * @param isFirstOr whether it is the first LogicOrExpression encountered
-   * @return global time predicate
+   * @return the extracted global time predicate, residual predicate and 
value-filter flag
    * @throws UnknownExpressionTypeException unknown expression type
    */
-  public static Pair<Expression, Boolean> extractGlobalTimePredicate(
-      Expression predicate, boolean canRewrite, boolean isFirstOr) {
+  public static GlobalTimePredicateExtractionResult extractGlobalTimePredicate(
+      Expression predicate) {
     if (predicate.getExpressionType().equals(ExpressionType.LOGIC_AND)) {
-      Pair<Expression, Boolean> leftResultPair =
-          extractGlobalTimePredicate(
-              ((BinaryExpression) predicate).getLeftExpression(), canRewrite, 
isFirstOr);
-      Pair<Expression, Boolean> rightResultPair =
-          extractGlobalTimePredicate(
-              ((BinaryExpression) predicate).getRightExpression(), canRewrite, 
isFirstOr);
-
-      // rewrite predicate to avoid duplicate calculation on time filter
-      // If Left-child or Right-child does not contain value filter
-      // We can set it to true in Predicate Tree
-      if (canRewrite) {
-        if (leftResultPair.left != null && !leftResultPair.right) {
-          ((BinaryExpression) predicate)
-              .setLeftExpression(new ConstantOperand(TSDataType.BOOLEAN, 
"true"));
-        }
-        if (rightResultPair.left != null && !rightResultPair.right) {
-          ((BinaryExpression) predicate)
-              .setRightExpression(new ConstantOperand(TSDataType.BOOLEAN, 
"true"));
-        }
-      }
-
-      if (leftResultPair.left != null && rightResultPair.left != null) {
-        return new Pair<>(
-            ExpressionFactory.and(leftResultPair.left, rightResultPair.left),
-            leftResultPair.right || rightResultPair.right);
-      } else if (leftResultPair.left != null) {
-        return new Pair<>(leftResultPair.left, true);
-      } else if (rightResultPair.left != null) {
-        return new Pair<>(rightResultPair.left, true);
-      }
-      return new Pair<>(null, true);
+      GlobalTimePredicateExtractionResult leftResult =
+          extractGlobalTimePredicate(((BinaryExpression) 
predicate).getLeftExpression());
+      GlobalTimePredicateExtractionResult rightResult =
+          extractGlobalTimePredicate(((BinaryExpression) 
predicate).getRightExpression());
+
+      return new GlobalTimePredicateExtractionResult(
+          combineConjuncts(
+              leftResult.getGlobalTimePredicate(), 
rightResult.getGlobalTimePredicate(), null),
+          combineConjuncts(
+              leftResult.getResidualPredicate(),
+              rightResult.getResidualPredicate(),
+              ConstantOperand.TRUE),
+          leftResult.hasValueFilter() || rightResult.hasValueFilter());
     } else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_OR)) {
-      Pair<Expression, Boolean> leftResultPair =
-          extractGlobalTimePredicate(
-              ((BinaryExpression) predicate).getLeftExpression(), false, 
false);
-      Pair<Expression, Boolean> rightResultPair =
-          extractGlobalTimePredicate(
-              ((BinaryExpression) predicate).getRightExpression(), false, 
false);
-
-      if (leftResultPair.left != null && rightResultPair.left != null) {
-        if (Boolean.TRUE.equals(isFirstOr && !leftResultPair.right && 
!rightResultPair.right)) {
-          ((BinaryExpression) predicate)
-              .setLeftExpression(new ConstantOperand(TSDataType.BOOLEAN, 
"true"));
-          ((BinaryExpression) predicate)
-              .setRightExpression(new ConstantOperand(TSDataType.BOOLEAN, 
"true"));
+      GlobalTimePredicateExtractionResult leftResult =
+          extractGlobalTimePredicate(((BinaryExpression) 
predicate).getLeftExpression());
+      GlobalTimePredicateExtractionResult rightResult =
+          extractGlobalTimePredicate(((BinaryExpression) 
predicate).getRightExpression());
+
+      if (leftResult.getGlobalTimePredicate() != null
+          && rightResult.getGlobalTimePredicate() != null) {
+        Expression globalTimePredicate =
+            ExpressionFactory.or(
+                leftResult.getGlobalTimePredicate(), 
rightResult.getGlobalTimePredicate());
+        boolean hasValueFilter = leftResult.hasValueFilter() || 
rightResult.hasValueFilter();
+        if (!hasValueFilter) {
+          return new GlobalTimePredicateExtractionResult(
+              globalTimePredicate, ConstantOperand.TRUE, false);
         }
-        return new Pair<>(
-            ExpressionFactory.or(leftResultPair.left, rightResultPair.left),
-            leftResultPair.right || rightResultPair.right);
+
+        // (G1 AND R1) OR (G2 AND R2) implies G1 OR G2, but R1 OR R2 is not an
+        // equivalent residual. Keeping the original OR predicate preserves
+        // P == (G1 OR G2) AND P without copying or mutating its subtrees.
+        return new GlobalTimePredicateExtractionResult(globalTimePredicate, 
predicate, true);
       }
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_NOT)) 
{
-      Pair<Expression, Boolean> childResultPair =
-          extractGlobalTimePredicate(
-              ((UnaryExpression) predicate).getExpression(), canRewrite, 
isFirstOr);
-      if (childResultPair.left != null) {
-        return new Pair<>(ExpressionFactory.not(childResultPair.left), 
childResultPair.right);
+      GlobalTimePredicateExtractionResult childResult =
+          extractGlobalTimePredicate(((UnaryExpression) 
predicate).getExpression());
+      if (childResult.getGlobalTimePredicate() != null && 
!childResult.hasValueFilter()) {
+        return new GlobalTimePredicateExtractionResult(
+            ExpressionFactory.not(childResult.getGlobalTimePredicate()),
+            ConstantOperand.TRUE,
+            false);
       }
-      return new Pair<>(null, true);
+      // NOT(G AND R) cannot in general be represented as NOT(G) AND NOT(R). Do
+      // not extract a time predicate when the negated subtree contains a value
+      // filter.
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.isCompareBinaryExpression()) {
       Expression leftExpression = ((BinaryExpression) 
predicate).getLeftExpression();
       Expression rightExpression = ((BinaryExpression) 
predicate).getRightExpression();
       if (checkIsTimeFilter(leftExpression, rightExpression)
           || checkIsTimeFilter(rightExpression, leftExpression)) {
-        return new Pair<>(predicate, false);
+        return new GlobalTimePredicateExtractionResult(predicate, 
ConstantOperand.TRUE, false);
       }
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.LIKE)
         || predicate.getExpressionType().equals(ExpressionType.REGEXP)) {
       // time filter don't support LIKE and REGEXP
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.BETWEEN)) {
       Expression firstExpression = ((TernaryExpression) 
predicate).getFirstExpression();
       Expression secondExpression = ((TernaryExpression) 
predicate).getSecondExpression();
@@ -162,31 +158,68 @@ public class PredicateUtils {
         isTimeFilter = checkBetweenConstantSatisfy(secondExpression, 
firstExpression);
       }
       if (isTimeFilter) {
-        return new Pair<>(predicate, false);
+        return new GlobalTimePredicateExtractionResult(predicate, 
ConstantOperand.TRUE, false);
       }
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.IS_NULL)) {
       // time filter don't support IS_NULL
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.IN)) {
       Expression timeExpression = ((InExpression) predicate).getExpression();
       if (timeExpression.getExpressionType().equals(ExpressionType.TIMESTAMP)) 
{
-        return new Pair<>(predicate, false);
+        return new GlobalTimePredicateExtractionResult(predicate, 
ConstantOperand.TRUE, false);
       }
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (predicate.getExpressionType().equals(ExpressionType.TIMESERIES)
         || predicate.getExpressionType().equals(ExpressionType.CONSTANT)
         || predicate.getExpressionType().equals(ExpressionType.NULL)) {
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if 
(predicate.getExpressionType().equals(ExpressionType.CASE_WHEN_THEN)) {
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else if (ExpressionType.FUNCTION.equals(predicate.getExpressionType())) {
-      return new Pair<>(null, true);
+      return new GlobalTimePredicateExtractionResult(null, predicate, true);
     } else {
       throw new UnknownExpressionTypeException(predicate.getExpressionType());
     }
   }
 
+  private static Expression combineConjuncts(
+      Expression leftExpression, Expression rightExpression, Expression 
identity) {
+    if (leftExpression == null || leftExpression.equals(identity)) {
+      return rightExpression;
+    }
+    if (rightExpression == null || rightExpression.equals(identity)) {
+      return leftExpression;
+    }
+    return ExpressionFactory.and(leftExpression, rightExpression);
+  }
+
+  public static final class GlobalTimePredicateExtractionResult {
+
+    private final Expression globalTimePredicate;
+    private final Expression residualPredicate;
+    private final boolean hasValueFilter;
+
+    private GlobalTimePredicateExtractionResult(
+        Expression globalTimePredicate, Expression residualPredicate, boolean 
hasValueFilter) {
+      this.globalTimePredicate = globalTimePredicate;
+      this.residualPredicate = residualPredicate;
+      this.hasValueFilter = hasValueFilter;
+    }
+
+    public Expression getGlobalTimePredicate() {
+      return globalTimePredicate;
+    }
+
+    public Expression getResidualPredicate() {
+      return residualPredicate;
+    }
+
+    public boolean hasValueFilter() {
+      return hasValueFilter;
+    }
+  }
+
   private static boolean checkIsTimeFilter(Expression timeExpression, 
Expression valueExpression) {
     return timeExpression.getExpressionType().equals(ExpressionType.TIMESTAMP)
         && valueExpression instanceof ConstantOperand
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java
index e1bef50380f..0de197964e2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java
@@ -40,6 +40,7 @@ import org.apache.tsfile.write.schema.IMeasurementSchema;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -70,6 +71,26 @@ public class TemplatedAggregationAnalyze {
       MPPQueryContext context,
       Template template,
       List<PartialPath> deviceList) {
+    return canBuildAggregationPlanUseTemplate(
+        analysis,
+        queryStatement,
+        partitionFetcher,
+        schemaTree,
+        context,
+        template,
+        deviceList,
+        new TreeAnalysisMutationJournal());
+  }
+
+  static boolean canBuildAggregationPlanUseTemplate(
+      Analysis analysis,
+      QueryStatement queryStatement,
+      IPartitionFetcher partitionFetcher,
+      ISchemaTree schemaTree,
+      MPPQueryContext context,
+      Template template,
+      List<PartialPath> deviceList,
+      TreeAnalysisMutationJournal mutationJournal) {
 
     // not support order by expression and non-aligned template
     if (queryStatement.hasOrderByExpression() || !template.isDirectAligned()) {
@@ -78,13 +99,6 @@ public class TemplatedAggregationAnalyze {
 
     analysis.setNoWhereAndAggregation(false);
 
-    if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
-      // remove the device which won't appear in resultSet after limit/offset
-      deviceList =
-          pushDownLimitOffsetInGroupByTimeForDevice(
-              deviceList, queryStatement, context.getZoneId());
-    }
-
     List<Pair<Expression, String>> outputExpressions = new ArrayList<>();
     boolean valid = analyzeSelect(queryStatement, analysis, outputExpressions, 
template);
     if (!valid) {
@@ -112,6 +126,21 @@ public class TemplatedAggregationAnalyze {
       return false;
     }
 
+    if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
+      // Pushdown rewrites LIMIT/OFFSET and the GROUP BY time range in the 
statement. Do it only
+      // after every template eligibility check succeeds, because the regular 
analyzer consumes the
+      // same statement when this optimization falls back.
+      mutationJournal.recordLimitOffsetPushDown(queryStatement);
+      deviceList =
+          pushDownLimitOffsetInGroupByTimeForDevice(
+              deviceList, queryStatement, context.getZoneId());
+      if (deviceList.isEmpty()) {
+        analysis.setFinishQueryAfterAnalyze(true);
+        return true;
+      }
+      analysis.setDeviceList(deviceList);
+    }
+
     analyzeDeviceToExpressions(analysis);
 
     analyzeDeviceViewOutput(analysis, queryStatement);
@@ -151,13 +180,29 @@ public class TemplatedAggregationAnalyze {
       if (selectExpression instanceof FunctionExpression
           && COUNT_TIME.equalsIgnoreCase(
               ((FunctionExpression) selectExpression).getFunctionName())) {
-        outputExpressions.add(new Pair<>(selectExpression, 
resultColumn.getAlias()));
-        selectExpressions.add(selectExpression);
-        aggregationExpressions.add(selectExpression);
+        FunctionExpression countTimeExpression = (FunctionExpression) 
selectExpression;
+        // The parsed count_time(*) expression belongs to the statement and 
may be analyzed again
+        // after a dispatch failure. Build a template-only expression instead 
of replacing its
+        // wildcard child in place. Prime the copy's expression string before 
replacing the child:
+        // the result header must remain count_time(*), while the planning 
output symbol must be
+        // count_time(Time), just as it was when the parsed expression was 
mutated in place.
+        FunctionExpression templatedCountTimeExpression =
+            new FunctionExpression(
+                countTimeExpression.getFunctionName(),
+                new 
LinkedHashMap<>(countTimeExpression.getFunctionAttributes()),
+                new ArrayList<>(countTimeExpression.getExpressions()));
+        templatedCountTimeExpression.getExpressionString();
+        templatedCountTimeExpression.setExpressions(
+            Collections.singletonList(new TimestampOperand()));
+        
templatedCountTimeExpression.setFunctionType(countTimeExpression.getFunctionType());
+
+        outputExpressions.add(new Pair<>(templatedCountTimeExpression, 
resultColumn.getAlias()));
+        selectExpressions.add(templatedCountTimeExpression);
+        aggregationExpressions.add(templatedCountTimeExpression);
 
-        analysis.getExpressionTypes().put(NodeRef.of(selectExpression), 
TSDataType.INT64);
-        ((FunctionExpression) selectExpression)
-            .setExpressions(Collections.singletonList(new TimestampOperand()));
+        analysis
+            .getExpressionTypes()
+            .put(NodeRef.of(templatedCountTimeExpression), TSDataType.INT64);
         continue;
       }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
index f3eefda1446..350cc5d090e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
@@ -98,7 +98,8 @@ public class TemplatedAnalyze {
       IPartitionFetcher partitionFetcher,
       ISchemaTree schemaTree,
       MPPQueryContext context,
-      List<PartialPath> deviceList) {
+      List<PartialPath> deviceList,
+      TreeAnalysisMutationJournal mutationJournal) {
     if (queryStatement.getGroupByComponent() != null
         || queryStatement.isSelectInto()
         || queryStatement.hasFill()
@@ -115,7 +116,14 @@ public class TemplatedAnalyze {
 
     if (queryStatement.isAggregationQuery()) {
       return canBuildAggregationPlanUseTemplate(
-          analysis, queryStatement, partitionFetcher, schemaTree, context, 
template, deviceList);
+          analysis,
+          queryStatement,
+          partitionFetcher,
+          schemaTree,
+          context,
+          template,
+          deviceList,
+          mutationJournal);
     }
 
     List<Pair<Expression, String>> outputExpressions = new ArrayList<>();
@@ -189,7 +197,8 @@ public class TemplatedAnalyze {
     }
     analysis.setDeviceList(deviceList);
 
-    analyzeDeviceToOrderBy(analysis, queryStatement, schemaTree, deviceList, 
context);
+    analyzeDeviceToOrderBy(
+        analysis, queryStatement, schemaTree, deviceList, context, 
mutationJournal);
     analyzeDeviceToSourceTransform(analysis);
     analyzeDeviceToSource(analysis);
 
@@ -277,7 +286,8 @@ public class TemplatedAnalyze {
       QueryStatement queryStatement,
       ISchemaTree schemaTree,
       List<PartialPath> deviceSet,
-      MPPQueryContext queryContext) {
+      MPPQueryContext queryContext,
+      TreeAnalysisMutationJournal mutationJournal) {
     if (!queryStatement.hasOrderByExpression()) {
       return;
     }
@@ -325,7 +335,7 @@ public class TemplatedAnalyze {
     }
 
     analysis.setOrderByExpressions(deviceViewOrderByExpression);
-    queryStatement.updateSortItems(deviceViewOrderByExpression);
+    mutationJournal.updateSortItems(queryStatement, 
deviceViewOrderByExpression);
     analysis.setDeviceToSortItems(deviceToSortItems);
     analysis.setDeviceToOrderByExpressions(deviceToOrderByExpressions);
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.java
new file mode 100644
index 00000000000..767afd2d215
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.queryengine.plan.analyze;
+
+import org.apache.iotdb.db.queryengine.plan.expression.Expression;
+import 
org.apache.iotdb.db.queryengine.plan.statement.component.GroupByTimeComponent;
+import 
org.apache.iotdb.db.queryengine.plan.statement.component.OrderByComponent;
+import org.apache.iotdb.db.queryengine.plan.statement.component.ResultColumn;
+import org.apache.iotdb.db.queryengine.plan.statement.component.WhereCondition;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Records the parser-owned state changed by one tree-query analysis attempt.
+ *
+ * <p>A retryable dispatch failure causes {@code QueryExecution} to analyze 
the same statement
+ * object again. The journal records only the references and primitive values 
that an analysis
+ * attempt replaces. Objects reachable from a recorded reference must 
therefore remain untouched;
+ * callers that need to change a nested mutable object must first obtain a 
copy-on-write working
+ * object from this class.
+ *
+ * <p>The journal belongs to the planner for one query execution. It is not a 
general AST snapshot
+ * and must not be used by cached relational prepared-statement ASTs.
+ */
+public final class TreeAnalysisMutationJournal {
+
+  private static final int SELECT_RESULT_COLUMNS = 1;
+  private static final int WHERE_CONDITION = 1 << 1;
+  private static final int GROUP_BY_CONTROL_EXPRESSION = 1 << 2;
+  private static final int ORDER_BY_COMPONENT = 1 << 3;
+  private static final int LIMIT_OFFSET = 1 << 4;
+  private static final int COUNT_TIME_AGGREGATION = 1 << 5;
+
+  private boolean active;
+  private int recordedFields;
+  private QueryStatement statement;
+
+  private List<ResultColumn> originalResultColumns;
+  private WhereCondition originalWhereCondition;
+  private Expression originalGroupByControlExpression;
+  private OrderByComponent originalOrderByComponent;
+  private boolean orderByDetached;
+
+  private long originalRowLimit;
+  private long originalRowOffset;
+  private boolean originalResultSetEmpty;
+  private GroupByTimeComponent originalGroupByTimeComponent;
+  private long originalGroupByStartTime;
+  private long originalGroupByEndTime;
+  private boolean originalCountTimeAggregation;
+
+  /** Starts a new attempt, rolling back an unfinished preceding attempt if 
necessary. */
+  public void begin() {
+    if (active) {
+      rollback();
+    }
+    active = true;
+  }
+
+  /**
+   * Makes repeated direct Analyzer invocations behave like QueryExecution 
retry attempts.
+   *
+   * <p>Production code starts and finishes attempts through the planner 
lifecycle. Unit tests and a
+   * few internal callers invoke Analyzer directly, so Analyzer calls this 
method before walking the
+   * statement.
+   */
+  public void prepareForAnalyze() {
+    if (!active) {
+      active = true;
+    } else if (recordedFields != 0) {
+      rollback();
+      active = true;
+    }
+  }
+
+  /** Restores the parser-owned state and releases all references held by this 
attempt. */
+  public void rollback() {
+    if (!active) {
+      return;
+    }
+    if (statement != null) {
+      if (isRecorded(SELECT_RESULT_COLUMNS)) {
+        statement.getSelectComponent().setResultColumns(originalResultColumns);
+      }
+      if (isRecorded(WHERE_CONDITION)) {
+        statement.setWhereCondition(originalWhereCondition);
+      }
+      if (isRecorded(GROUP_BY_CONTROL_EXPRESSION)) {
+        statement
+            .getGroupByComponent()
+            
.setControlColumnExpressionForAnalyze(originalGroupByControlExpression);
+      }
+      if (isRecorded(ORDER_BY_COMPONENT)) {
+        statement.setOrderByComponent(originalOrderByComponent);
+      }
+      if (isRecorded(LIMIT_OFFSET)) {
+        statement.setRowLimit(originalRowLimit);
+        statement.setRowOffset(originalRowOffset);
+        statement.setResultSetEmpty(originalResultSetEmpty);
+        originalGroupByTimeComponent.setStartTime(originalGroupByStartTime);
+        originalGroupByTimeComponent.setEndTime(originalGroupByEndTime);
+      }
+      if (isRecorded(COUNT_TIME_AGGREGATION)) {
+        statement.setCountTimeAggregation(originalCountTimeAggregation);
+      }
+    }
+    clear();
+  }
+
+  /**
+   * Commits an attempt by discarding its undo information.
+   *
+   * <p>The working statement is still referenced by Analysis and the 
generated plans, so a
+   * successful attempt is not restored after the dispatch retry window has 
closed.
+   */
+  public void commit() {
+    clear();
+  }
+
+  public void replaceResultColumns(
+      QueryStatement queryStatement, List<ResultColumn> resultColumns) {
+    ensureActive(queryStatement);
+    if (!isRecorded(SELECT_RESULT_COLUMNS)) {
+      originalResultColumns = 
queryStatement.getSelectComponent().getResultColumns();
+      recordedFields |= SELECT_RESULT_COLUMNS;
+    }
+    queryStatement.getSelectComponent().setResultColumns(resultColumns);
+  }
+
+  public void replaceWhereCondition(QueryStatement queryStatement, 
WhereCondition whereCondition) {
+    ensureActive(queryStatement);
+    if (!isRecorded(WHERE_CONDITION)) {
+      originalWhereCondition = queryStatement.getWhereCondition();
+      recordedFields |= WHERE_CONDITION;
+    }
+    queryStatement.setWhereCondition(whereCondition);
+  }
+
+  public void replaceGroupByControlExpression(
+      QueryStatement queryStatement, Expression controlExpression) {
+    ensureActive(queryStatement);
+    if (!isRecorded(GROUP_BY_CONTROL_EXPRESSION)) {
+      originalGroupByControlExpression =
+          queryStatement.getGroupByComponent().getControlColumnExpression();
+      recordedFields |= GROUP_BY_CONTROL_EXPRESSION;
+    }
+    
queryStatement.getGroupByComponent().setControlColumnExpressionForAnalyze(controlExpression);
+  }
+
+  /**
+   * Returns an attempt-local ORDER BY component whose nested lists and 
SortItems may be changed.
+   */
+  public OrderByComponent getMutableOrderByComponent(QueryStatement 
queryStatement) {
+    ensureActive(queryStatement);
+    if (queryStatement.getOrderByComponent() == null) {
+      return null;
+    }
+    recordOrderByComponent(queryStatement);
+    if (!orderByDetached) {
+      queryStatement.setOrderByComponent(
+          OrderByComponent.copyOf(queryStatement.getOrderByComponent()));
+      orderByDetached = true;
+    }
+    return queryStatement.getOrderByComponent();
+  }
+
+  public void clearOrderByComponent(QueryStatement queryStatement) {
+    ensureActive(queryStatement);
+    recordOrderByComponent(queryStatement);
+    queryStatement.setOrderByComponent(null);
+  }
+
+  public void updateSortItems(QueryStatement queryStatement, Set<Expression> 
orderByExpressions) {
+    if (getMutableOrderByComponent(queryStatement) != null) {
+      queryStatement.updateSortItems(orderByExpressions);
+    }
+  }
+
+  /** Records all values changed together by LIMIT/OFFSET pushdown. */
+  public void recordLimitOffsetPushDown(QueryStatement queryStatement) {
+    ensureActive(queryStatement);
+    if (isRecorded(LIMIT_OFFSET)) {
+      return;
+    }
+    originalRowLimit = queryStatement.getRowLimit();
+    originalRowOffset = queryStatement.getRowOffset();
+    originalResultSetEmpty = queryStatement.isResultSetEmpty();
+    originalGroupByTimeComponent = queryStatement.getGroupByTimeComponent();
+    originalGroupByStartTime = originalGroupByTimeComponent.getStartTime();
+    originalGroupByEndTime = originalGroupByTimeComponent.getEndTime();
+    recordedFields |= LIMIT_OFFSET;
+  }
+
+  /** Records the derived flag that semantic validation sets for {@code 
count_time(*)}. */
+  public void recordCountTimeAggregationChange(
+      QueryStatement queryStatement, boolean originalValue) {
+    ensureActive(queryStatement);
+    if (!isRecorded(COUNT_TIME_AGGREGATION)) {
+      originalCountTimeAggregation = originalValue;
+      recordedFields |= COUNT_TIME_AGGREGATION;
+    }
+  }
+
+  private void recordOrderByComponent(QueryStatement queryStatement) {
+    if (!isRecorded(ORDER_BY_COMPONENT)) {
+      originalOrderByComponent = queryStatement.getOrderByComponent();
+      recordedFields |= ORDER_BY_COMPONENT;
+    }
+  }
+
+  private boolean isRecorded(int field) {
+    return (recordedFields & field) != 0;
+  }
+
+  private void ensureActive(QueryStatement queryStatement) {
+    if (!active) {
+      active = true;
+    }
+    if (statement == null) {
+      statement = queryStatement;
+    } else if (statement != queryStatement) {
+      throw new IllegalStateException();
+    }
+  }
+
+  private void clear() {
+    active = false;
+    recordedFields = 0;
+    statement = null;
+    originalResultColumns = null;
+    originalWhereCondition = null;
+    originalGroupByControlExpression = null;
+    originalOrderByComponent = null;
+    orderByDetached = false;
+    originalGroupByTimeComponent = null;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
index 96da558b81a..037ea8688bf 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
@@ -128,7 +128,13 @@ public class QueryExecution implements IQueryExecution {
   public QueryExecution(IPlanner planner, MPPQueryContext context, 
ExecutorService executor) {
     this.context = context;
     this.planner = planner;
-    this.analysis = analyze(context);
+    planner.beginAnalysisAttempt();
+    try {
+      this.analysis = analyze(context);
+    } catch (RuntimeException | Error e) {
+      planner.rollbackAnalysisAttempt();
+      throw e;
+    }
     context.setNeedSetHighestPriority(analysis.needSetHighestPriority());
     this.stateMachine = new QueryStateMachine(context.getQueryId(), executor);
 
@@ -166,6 +172,15 @@ public class QueryExecution implements IQueryExecution {
 
   @Override
   public void start() {
+    try {
+      startInternal();
+    } catch (RuntimeException | Error e) {
+      finishAnalysisAttemptForCurrentState();
+      throw e;
+    }
+  }
+
+  private void startInternal() {
     final long startTime = System.nanoTime();
     if (skipExecute()) {
       LOGGER.debug(DataNodeQueryMessages.SKIP_EXECUTE);
@@ -175,6 +190,7 @@ public class QueryExecution implements IQueryExecution {
         constructResultForMemorySource();
         stateMachine.transitionToRunning();
       }
+      finishAnalysisAttemptForCurrentState();
       return;
     }
 
@@ -191,6 +207,7 @@ public class QueryExecution implements IQueryExecution {
         constructResultForMemorySource();
         stateMachine.transitionToRunning();
       }
+      finishAnalysisAttemptForCurrentState();
       return;
     }
 
@@ -216,6 +233,7 @@ public class QueryExecution implements IQueryExecution {
     if (!context.isQuery() && analysis.isFailed()) {
       stateMachine.transitionToFailed(analysis.getFailStatus());
     }
+    finishAnalysisAttemptForCurrentState();
   }
 
   private void checkTimeOutForQuery() {
@@ -229,12 +247,19 @@ public class QueryExecution implements IQueryExecution {
   private ExecutionResult retry() {
     if (retryCount >= MAX_RETRY_COUNT) {
       LOGGER.warn(DataNodeQueryMessages.REACHMAXRETRYCOUNT);
+      // Keep the same ownership order as an ordinary retry: plans and 
scheduler stop using the
+      // analysis working state before the journal restores parser-owned 
objects.
+      this.stopAndCleanup(stateMachine.getFailureException());
+      planner.rollbackAnalysisAttempt();
       stateMachine.transitionToFailed();
       return getStatus();
     }
     LOGGER.warn(DataNodeQueryMessages.ERROR_WHEN_EXECUTING_QUERY, 
stateMachine.getFailureMessage());
     // stop and clean up resources the QueryExecution used
     this.stopAndCleanup(stateMachine.getFailureException());
+    // The generated plans no longer read the statement after cleanup. Restore 
the parser-owned
+    // state before the next analysis attempt so retry observes the same SQL 
as the first attempt.
+    planner.rollbackAnalysisAttempt();
     LOGGER.info(DataNodeQueryMessages.WAITBEFORERETRY_WAIT_MS, 
RETRY_INTERVAL_IN_MS);
     try {
       Thread.sleep(RETRY_INTERVAL_IN_MS);
@@ -253,12 +278,30 @@ public class QueryExecution implements IQueryExecution {
     this.stopped.compareAndSet(true, false);
     this.resultHandleCleanUp.compareAndSet(true, false);
     // re-analyze the query
-    this.analysis = analyze(context);
+    planner.beginAnalysisAttempt();
+    try {
+      this.analysis = analyze(context);
+    } catch (RuntimeException | Error e) {
+      planner.rollbackAnalysisAttempt();
+      throw e;
+    }
     // re-start the QueryExecution
     this.start();
     return getStatus();
   }
 
+  private void finishAnalysisAttemptForCurrentState() {
+    QueryState state = stateMachine.getState();
+    if (state == QueryState.PENDING_RETRY) {
+      return;
+    }
+    if (state == QueryState.RUNNING || state == QueryState.FINISHED) {
+      planner.commitAnalysisAttempt();
+    } else {
+      planner.rollbackAnalysisAttempt();
+    }
+  }
+
   private boolean skipExecute() {
     return analysis.canSkipExecute(context);
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
index be81a33587c..b2518f7a7e6 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
@@ -22,6 +22,7 @@ package 
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate;
 import org.apache.iotdb.db.queryengine.plan.expression.Expression;
 import org.apache.iotdb.db.queryengine.plan.expression.ExpressionType;
 import 
org.apache.iotdb.db.queryengine.plan.expression.binary.ArithmeticBinaryExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.binary.BinaryExpression;
 import 
org.apache.iotdb.db.queryengine.plan.expression.binary.CompareBinaryExpression;
 import 
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicAndExpression;
 import 
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicOrExpression;
@@ -45,6 +46,10 @@ import 
org.apache.iotdb.db.queryengine.plan.expression.visitor.ExpressionAnalyze
 import java.util.ArrayList;
 import java.util.List;
 
+import static 
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructBinaryExpression;
+import static 
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructFunctionExpression;
+import static 
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructTernaryExpression;
+import static 
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructUnaryExpression;
 import static 
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate.ConvertPredicateToFilterVisitor.getValue;
 
 public class PredicateSimplifier extends ExpressionAnalyzeVisitor<Expression, 
Void> {
@@ -59,8 +64,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
         return ConstantOperand.TRUE;
       }
     }
-    isNullExpression.setExpression(simplifiedInputExpression);
-    return isNullExpression;
+    return reconstructUnaryIfNeeded(isNullExpression, 
simplifiedInputExpression);
   }
 
   @Override
@@ -83,8 +87,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
     if 
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
       return ConstantOperand.FALSE;
     }
-    unaryExpression.setExpression(simplifiedInputExpression);
-    return unaryExpression;
+    return reconstructUnaryIfNeeded(unaryExpression, 
simplifiedInputExpression);
   }
 
   @Override
@@ -96,8 +99,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
     if (simplifiedInputExpression.equals(ConstantOperand.FALSE)) {
       return ConstantOperand.TRUE;
     }
-    logicNotExpression.setExpression(simplifiedInputExpression);
-    return logicNotExpression;
+    return reconstructUnaryIfNeeded(logicNotExpression, 
simplifiedInputExpression);
   }
 
   @Override
@@ -106,8 +108,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
     if 
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
       return new NullOperand();
     }
-    negationExpression.setExpression(simplifiedInputExpression);
-    return negationExpression;
+    return reconstructUnaryIfNeeded(negationExpression, 
simplifiedInputExpression);
   }
 
   @Override
@@ -121,9 +122,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
       // null calculation will return null
       return new NullOperand();
     }
-    arithmeticBinaryExpression.setLeftExpression(simplifiedLeft);
-    arithmeticBinaryExpression.setRightExpression(simplifiedRight);
-    return arithmeticBinaryExpression;
+    return reconstructBinaryIfNeeded(arithmeticBinaryExpression, 
simplifiedLeft, simplifiedRight);
   }
 
   @Override
@@ -144,9 +143,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
     if (isLeftFalse || isRightFalse) {
       return ConstantOperand.FALSE;
     }
-    logicAndExpression.setLeftExpression(simplifiedLeft);
-    logicAndExpression.setRightExpression(simplifiedRight);
-    return logicAndExpression;
+    return reconstructBinaryIfNeeded(logicAndExpression, simplifiedLeft, 
simplifiedRight);
   }
 
   @Override
@@ -167,9 +164,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
     } else if (isRightFalse) {
       return simplifiedLeft;
     }
-    logicOrExpression.setLeftExpression(simplifiedLeft);
-    logicOrExpression.setRightExpression(simplifiedRight);
-    return logicOrExpression;
+    return reconstructBinaryIfNeeded(logicOrExpression, simplifiedLeft, 
simplifiedRight);
   }
 
   @Override
@@ -183,9 +178,7 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
       // null comparison will return false
       return ConstantOperand.FALSE;
     }
-    compareBinaryExpression.setLeftExpression(simplifiedLeft);
-    compareBinaryExpression.setRightExpression(simplifiedRight);
-    return compareBinaryExpression;
+    return reconstructBinaryIfNeeded(compareBinaryExpression, simplifiedLeft, 
simplifiedRight);
   }
 
   @Override
@@ -228,10 +221,16 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
         return ConstantOperand.FALSE;
       }
     }
-    betweenExpression.setFirstExpression(simplifiedFirstExpression);
-    betweenExpression.setSecondExpression(simplifiedSecondExpression);
-    betweenExpression.setThirdExpression(simplifiedThirdExpression);
-    return betweenExpression;
+    if (simplifiedFirstExpression == betweenExpression.getFirstExpression()
+        && simplifiedSecondExpression == 
betweenExpression.getSecondExpression()
+        && simplifiedThirdExpression == 
betweenExpression.getThirdExpression()) {
+      return betweenExpression;
+    }
+    return reconstructTernaryExpression(
+        betweenExpression,
+        simplifiedFirstExpression,
+        simplifiedSecondExpression,
+        simplifiedThirdExpression);
   }
 
   private <T extends Comparable<T>> boolean lessThan(
@@ -243,16 +242,42 @@ public class PredicateSimplifier extends 
ExpressionAnalyzeVisitor<Expression, Vo
 
   @Override
   public Expression visitFunctionExpression(FunctionExpression 
functionExpression, Void context) {
-    List<Expression> simplifiedInputExpressions = new ArrayList<>();
-    for (Expression inputExpression : functionExpression.getExpressions()) {
+    List<Expression> inputExpressions = functionExpression.getExpressions();
+    List<Expression> simplifiedInputExpressions = null;
+    for (int i = 0; i < inputExpressions.size(); i++) {
+      Expression inputExpression = inputExpressions.get(i);
       Expression simplifiedInputExpression = process(inputExpression, context);
       if 
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
         return new NullOperand();
       }
-      simplifiedInputExpressions.add(simplifiedInputExpression);
+      if (simplifiedInputExpression != inputExpression) {
+        if (simplifiedInputExpressions == null) {
+          simplifiedInputExpressions = new ArrayList<>(inputExpressions);
+        }
+        simplifiedInputExpressions.set(i, simplifiedInputExpression);
+      }
+    }
+    if (simplifiedInputExpressions == null) {
+      return functionExpression;
+    }
+    return reconstructFunctionExpression(functionExpression, 
simplifiedInputExpressions);
+  }
+
+  private Expression reconstructUnaryIfNeeded(
+      UnaryExpression originalExpression, Expression simplifiedChild) {
+    if (simplifiedChild == originalExpression.getExpression()) {
+      return originalExpression;
+    }
+    return reconstructUnaryExpression(originalExpression, simplifiedChild);
+  }
+
+  private Expression reconstructBinaryIfNeeded(
+      BinaryExpression originalExpression, Expression simplifiedLeft, 
Expression simplifiedRight) {
+    if (simplifiedLeft == originalExpression.getLeftExpression()
+        && simplifiedRight == originalExpression.getRightExpression()) {
+      return originalExpression;
     }
-    functionExpression.setExpressions(simplifiedInputExpressions);
-    return functionExpression;
+    return reconstructBinaryExpression(originalExpression, simplifiedLeft, 
simplifiedRight);
   }
 
   @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
index 501aa1e09d4..99cd988bbb7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
@@ -34,6 +34,12 @@ public interface IPlanner {
 
   IAnalysis analyze(MPPQueryContext context);
 
+  default void beginAnalysisAttempt() {}
+
+  default void rollbackAnalysisAttempt() {}
+
+  default void commitAnalysisAttempt() {}
+
   LogicalQueryPlan doLogicalPlan(IAnalysis analysis, MPPQueryContext context);
 
   DistributedQueryPlan doDistributionPlan(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java
index ce450b50052..f2c0011b801 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java
@@ -497,11 +497,11 @@ public class LogicalPlanBuilder {
             .map(Expression::getExpressionString)
             .collect(Collectors.toList());
 
-    List<SortItem> sortItemList = queryStatement.getSortItemList();
-
-    if (sortItemList.isEmpty()) {
-      sortItemList = new ArrayList<>();
-    }
+    // DEVICE and TIME are implicit merge keys for DeviceView. Append them to 
a planning-owned copy
+    // so rebuilding the plan after a dispatch failure does not mutate the 
parsed statement or
+    // accumulate duplicate keys. Keep the complete parameter in Analysis 
because a downstream
+    // SortNode must use the same implicit keys without reading a mutated 
QueryStatement.
+    List<SortItem> sortItemList = new 
ArrayList<>(queryStatement.getSortItemList());
     if (!queryStatement.isOrderByDevice()) {
       sortItemList.add(new SortItem(OrderByKey.DEVICE, Ordering.ASC));
     }
@@ -510,6 +510,7 @@ public class LogicalPlanBuilder {
     }
 
     OrderByParameter orderByParameter = new OrderByParameter(sortItemList);
+    analysis.setMergeOrderParameter(orderByParameter);
 
     long limitValue =
         queryStatement.hasOffset()
@@ -1431,7 +1432,10 @@ public class LogicalPlanBuilder {
 
     Set<Expression> orderByExpressions = analysis.getOrderByExpressions();
     updateTypeProvider(orderByExpressions);
-    OrderByParameter orderByParameter = new 
OrderByParameter(queryStatement.getSortItemList());
+    OrderByParameter orderByParameter =
+        queryStatement.isAlignByDevice()
+            ? analysis.getMergeOrderParameter()
+            : new OrderByParameter(queryStatement.getSortItemList());
     if (orderByParameter.isEmpty()) {
       return this;
     }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
index 756f17148ca..8a614809559 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
@@ -31,6 +31,7 @@ import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
 import org.apache.iotdb.db.queryengine.plan.analyze.Analyzer;
 import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
 import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
+import 
org.apache.iotdb.db.queryengine.plan.analyze.TreeAnalysisMutationJournal;
 import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
 import 
org.apache.iotdb.db.queryengine.plan.planner.distribution.DistributionPlanner;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
@@ -57,6 +58,7 @@ import static 
org.apache.iotdb.db.queryengine.metric.QueryPlanCostMetricSet.DIST
 public class TreeModelPlanner implements IPlanner {
 
   private final Statement statement;
+  private final TreeAnalysisMutationJournal mutationJournal = new 
TreeAnalysisMutationJournal();
 
   private final ExecutorService executor;
   private final ScheduledExecutorService scheduledExecutor;
@@ -91,7 +93,23 @@ public class TreeModelPlanner implements IPlanner {
 
   @Override
   public IAnalysis analyze(MPPQueryContext context) {
-    return new Analyzer(context, partitionFetcher, 
schemaFetcher).analyze(statement);
+    return new Analyzer(context, partitionFetcher, schemaFetcher, 
mutationJournal)
+        .analyze(statement);
+  }
+
+  @Override
+  public void beginAnalysisAttempt() {
+    mutationJournal.begin();
+  }
+
+  @Override
+  public void rollbackAnalysisAttempt() {
+    mutationJournal.rollback();
+  }
+
+  @Override
+  public void commitAnalysisAttempt() {
+    mutationJournal.commit();
   }
 
   @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
index 80e8bae032a..741116c844c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
@@ -5400,20 +5400,29 @@ public class StatementAnalyzer {
 
     @Override
     public Scope visitShowDevice(final ShowDevice node, final Optional<Scope> 
context) {
-      analyzeQueryDevice(node, context);
+      // SQL SHOW DEVICES statements retain their parser-owned AST and 
therefore need an isolated
+      // analysis copy. Schema fetching also uses ShowDevice internally, but 
that form has already
+      // been populated with tag, fuzzy, partition, and attribute filters; 
those fields are the
+      // input of the internal request rather than derived state and must not 
be discarded.
+      final ShowDevice workingNode =
+          Objects.nonNull(node.getTable()) ? node.copyForAnalysis() : node;
+      analysis.setStatement(workingNode);
+      analyzeQueryDevice(workingNode, context);
       // TODO: use real scope when parameter in offset and limit is supported
-      if (Objects.nonNull(node.getOffset())) {
-        analyzeOffset(node.getOffset(), null);
+      if (Objects.nonNull(workingNode.getOffset())) {
+        analyzeOffset(workingNode.getOffset(), null);
       }
-      if (Objects.nonNull(node.getLimit())) {
-        analyzeLimit(node.getLimit(), null);
+      if (Objects.nonNull(workingNode.getLimit())) {
+        analyzeLimit(workingNode.getLimit(), null);
       }
       return null;
     }
 
     @Override
     public Scope visitCountDevice(final CountDevice node, final 
Optional<Scope> context) {
-      analyzeQueryDevice(node, context);
+      final CountDevice workingNode = node.copyForAnalysis();
+      analysis.setStatement(workingNode);
+      analyzeQueryDevice(workingNode, context);
       return null;
     }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractQueryDeviceWithCache.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractQueryDeviceWithCache.java
index 8c3d46b2cca..91dae4cc76e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractQueryDeviceWithCache.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AbstractQueryDeviceWithCache.java
@@ -62,6 +62,30 @@ public abstract class AbstractQueryDeviceWithCache extends 
AbstractTraverseDevic
     super(database, tableName);
   }
 
+  /**
+   * Copies only the parsed input of a device query into a fresh analysis 
working statement.
+   *
+   * <p>The analyzer and schema fetcher populate {@link 
AbstractTraverseDevice} with resolved names,
+   * translated predicates, cache results, and partition filters. A parsed 
statement, however, may
+   * be retained by a prepared statement and analyzed again by another 
execution or by a dispatch
+   * retry. Cache results, column headers, partition keys, and tag/fuzzy 
filters are therefore not
+   * carried into the working statement, so every execution starts from the 
same parsed input.
+   *
+   * <p>The raw {@code where} tree is intentionally shared with the 
parser-owned statement. This
+   * analysis path treats it as read-only: expression rewriters return a 
replacement root (possibly
+   * reusing unchanged nodes), and only the working statement's {@code where} 
reference is replaced.
+   * Schema predicate parsing also only reads the AST and stores derived 
filters on the working
+   * statement. A deep expression copy would therefore add per-execution cost 
without improving
+   * retry isolation.
+   */
+  protected AbstractQueryDeviceWithCache(final AbstractQueryDeviceWithCache 
source) {
+    super(source.getLocation().orElse(null), source.table, source.where);
+    this.database = source.database;
+    this.tableName = source.tableName;
+  }
+
+  public abstract AbstractQueryDeviceWithCache copyForAnalysis();
+
   public boolean parseRawExpression(
       final TsTable tableInstance,
       final List<String> attributeColumns,
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/CountDevice.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/CountDevice.java
index b8800a31f4d..f6a71992640 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/CountDevice.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/CountDevice.java
@@ -46,6 +46,15 @@ public class CountDevice extends 
AbstractQueryDeviceWithCache {
     super(location, table, rawExpression);
   }
 
+  private CountDevice(final CountDevice source) {
+    super(source);
+  }
+
+  @Override
+  public CountDevice copyForAnalysis() {
+    return new CountDevice(this);
+  }
+
   @Override
   public DatasetHeader getDataSetHeader() {
     return new DatasetHeader(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ShowDevice.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ShowDevice.java
index 2a929cec541..0020ecf35aa 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ShowDevice.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ShowDevice.java
@@ -66,6 +66,17 @@ public class ShowDevice extends AbstractQueryDeviceWithCache 
{
     super(database, tableName);
   }
 
+  private ShowDevice(final ShowDevice source) {
+    super(source);
+    this.offset = source.offset;
+    this.limit = source.limit;
+  }
+
+  @Override
+  public ShowDevice copyForAnalysis() {
+    return new ShowDevice(this);
+  }
+
   public Offset getOffset() {
     return offset;
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
index ec45a90a4b8..b1d5fefde52 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
@@ -45,6 +45,11 @@ public abstract class GroupByComponent extends StatementNode 
{
         ExpressionAnalyzer.toLowerCaseExpression(controlColumnExpression);
   }
 
+  /** Sets an expression that has already been normalized by the analyzer. */
+  public void setControlColumnExpressionForAnalyze(Expression 
controlColumnExpression) {
+    this.controlColumnExpression = controlColumnExpression;
+  }
+
   public Expression getControlColumnExpression() {
     return controlColumnExpression;
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
index 1014e32df1d..8f07439b76e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
@@ -49,6 +49,36 @@ public class OrderByComponent extends StatementNode {
     this.sortItemExpressionList = new ArrayList<>();
   }
 
+  /**
+   * Creates a structurally independent component for analysis-time 
copy-on-write updates.
+   *
+   * <p>Expression nodes are shared because analysis replaces them instead of 
mutating them. Lists
+   * and SortItems are copied because both are changed in place while 
resolving ORDER BY
+   * expressions.
+   */
+  public static OrderByComponent copyOf(OrderByComponent source) {
+    OrderByComponent result = new OrderByComponent();
+    int expressionIndex = 0;
+    for (SortItem sortItem : source.sortItemList) {
+      if (sortItem.isExpression()) {
+        SortItem copiedSortItem =
+            new SortItem(
+                sortItem.getExpression(), sortItem.getOrdering(), 
sortItem.getNullOrdering());
+        result.sortItemList.add(copiedSortItem);
+        // SortItem intentionally keeps the parser-facing expression for SQL 
rendering, while the
+        // parallel expression list contains the lower-case normalized AST 
consumed by analysis.
+        // They are not interchangeable, so preserve both references 
independently in the COW
+        // component.
+        
result.sortItemExpressionList.add(source.sortItemExpressionList.get(expressionIndex++));
+      } else {
+        result.addSortItem(
+            new SortItem(
+                sortItem.getSortKey(), sortItem.getOrdering(), 
sortItem.getNullOrdering()));
+      }
+    }
+    return result;
+  }
+
   public void addSortItem(SortItem sortItem) {
     this.sortItemList.add(sortItem);
     switch (sortItem.getSortKey()) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
index 7352c1e96fc..d80677fd830 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
@@ -35,6 +35,13 @@ public class WhereCondition extends StatementNode {
     this.predicate = ExpressionAnalyzer.toLowerCaseExpression(predicate);
   }
 
+  /** Wraps an expression that has already been normalized without rebuilding 
its AST. */
+  public static WhereCondition fromNormalizedPredicate(Expression predicate) {
+    WhereCondition whereCondition = new WhereCondition();
+    whereCondition.predicate = predicate;
+    return whereCondition;
+  }
+
   public Expression getPredicate() {
     return predicate;
   }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
index 2cc9b8faf7e..12191b62970 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.queryengine.plan.analyze;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.common.rpc.thrift.TSeriesPartitionSlot;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.exception.SemanticException;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.queryengine.common.SessionInfo;
 import org.apache.iotdb.commons.schema.column.ColumnHeader;
@@ -648,6 +649,218 @@ public class AnalyzeTest {
     }
   }
 
+  @Test
+  public void testTimeFilterPreservedWhenQueryAnalysisIsRetried() throws 
Exception {
+    // In the reported incident, the first dispatch failed because the 
coordinator's internal
+    // client pool for the leader DataNode endpoint had reached its 
1000-client limit.
+    // QueryExecution handles that transient failure by analyzing the same 
Statement object again.
+    // Repeating analysis directly makes the regression deterministic and 
independent of the
+    // resource exhaustion and dispatch timing.
+    String[] sqls =
+        new String[] {
+          "select s1 from root.sg.d1 where time > 1",
+          "select s1 from root.sg.d1 where time > 1 and time < 3",
+          "select s1 from root.sg.d1 where time > 1 and time < 3 and s1 > 1"
+        };
+    boolean[] expectedHasValueFilter = new boolean[] {false, false, true};
+
+    for (int i = 0; i < sqls.length; i++) {
+      String sql = sqls[i];
+      Statement statement =
+          StatementGenerator.createStatement(sql, 
ZonedDateTime.now().getOffset());
+      MPPQueryContext context =
+          new MPPQueryContext(
+              "",
+              new QueryId("test_query_retry"),
+              new SessionInfo(0, "test", ZoneId.systemDefault()),
+              new TEndPoint(),
+              new TEndPoint());
+      Analyzer analyzer =
+          new Analyzer(context, new FakePartitionFetcherImpl(), new 
FakeSchemaFetcherImpl());
+
+      Analysis initialAnalysis = analyzer.analyze(statement);
+
+      Assert.assertNotNull(sql, initialAnalysis.getGlobalTimePredicate());
+      assertEquals(sql, expectedHasValueFilter[i], 
initialAnalysis.hasValueFilter());
+
+      // A second retry verifies that the same untouched snapshot can be 
restored repeatedly.
+      for (int retry = 0; retry < 2; retry++) {
+        context.prepareForRetry();
+        Analysis retriedAnalysis = analyzer.analyze(statement);
+
+        assertEquals(
+            sql,
+            initialAnalysis.getGlobalTimePredicate(),
+            retriedAnalysis.getGlobalTimePredicate());
+        assertEquals(
+            sql, initialAnalysis.getWhereExpression(), 
retriedAnalysis.getWhereExpression());
+        assertEquals(sql, expectedHasValueFilter[i], 
retriedAnalysis.hasValueFilter());
+      }
+    }
+  }
+
+  @Test
+  public void 
testCountTimeWithMultipleFromPathsPreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select count_time(*) from root.sg.d1, root.sg.d2");
+
+    assertEquals(analyses.left.getRespDatasetHeader(), 
analyses.right.getRespDatasetHeader());
+    assertEquals(analyses.left.getSelectExpressions(), 
analyses.right.getSelectExpressions());
+    assertEquals(
+        analyses.left.getAggregationExpressions(), 
analyses.right.getAggregationExpressions());
+  }
+
+  @Test
+  public void testFailedSemanticCheckRestoresCountTimeFlag() {
+    QueryStatement statement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "select count_time(*) from root.sg.d1 having count_time(*) > 
0",
+                ZonedDateTime.now().getOffset());
+    MPPQueryContext context =
+        new MPPQueryContext(
+            "",
+            new QueryId("test_count_time_semantic_failure"),
+            new SessionInfo(0, "test", ZoneId.systemDefault()),
+            new TEndPoint(),
+            new TEndPoint());
+
+    Assert.assertFalse(statement.isCountTimeAggregation());
+    try {
+      new Analyzer(context, new FakePartitionFetcherImpl(), new 
FakeSchemaFetcherImpl())
+          .analyze(statement);
+      fail("count_time with HAVING should fail semantic validation");
+    } catch (SemanticException expected) {
+      Assert.assertFalse(statement.isCountTimeAggregation());
+    }
+  }
+
+  @Test
+  public void testGroupByLevelHavingPreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select count(s1) from root.sg.d1 group by level = 1 
having count(s1) > 0");
+
+    assertEquals(analyses.left.getRespDatasetHeader(), 
analyses.right.getRespDatasetHeader());
+    assertEquals(analyses.left.getHavingExpression(), 
analyses.right.getHavingExpression());
+    assertEquals(
+        analyses.left.getCrossGroupByExpressions(), 
analyses.right.getCrossGroupByExpressions());
+  }
+
+  @Test
+  public void testLimitOffsetPushDownPreservedWhenQueryAnalysisIsRetried()
+      throws IllegalPathException {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice(
+            "select count(s1) from root.sg.* group by ([0, 100), 10ms) "
+                + "limit 10 offset 10 align by device");
+
+    assertEquals(
+        Collections.singletonList(new PartialPath("root.sg.d2")), 
analyses.left.getDeviceList());
+    assertEquals(analyses.left.getDeviceList(), 
analyses.right.getDeviceList());
+    assertEquals(analyses.left.getGroupByTimeParameter(), 
analyses.right.getGroupByTimeParameter());
+  }
+
+  @Test
+  public void testSelectIntoDeviceOrderPreservedWhenQueryAnalysisIsRetried()
+      throws IllegalPathException {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice(
+            "select s1 into root.copy.t1(x), root.copy.t2(x) from root.sg.* "
+                + "order by device desc align by device");
+
+    Map<String, List<Pair<String, PartialPath>>> expectedMapping = new 
HashMap<>();
+    expectedMapping.put(
+        "root.sg.d2",
+        Collections.singletonList(new Pair<>("s1", new 
PartialPath("root.copy.t1.x"))));
+    expectedMapping.put(
+        "root.sg.d1",
+        Collections.singletonList(new Pair<>("s1", new 
PartialPath("root.copy.t2.x"))));
+    assertEquals(
+        expectedMapping,
+        
analyses.left.getDeviceViewIntoPathDescriptor().getDeviceToSourceTargetPathPairListMap());
+    assertEquals(
+        
analyses.left.getDeviceViewIntoPathDescriptor().getDeviceToSourceTargetPathPairListMap(),
+        
analyses.right.getDeviceViewIntoPathDescriptor().getDeviceToSourceTargetPathPairListMap());
+  }
+
+  @Test
+  public void 
testNormalizedOrderByExpressionPreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select s1 from root.sg.* order by Sin(s1) align by 
device");
+
+    assertEquals(analyses.left.getOrderByExpressions(), 
analyses.right.getOrderByExpressions());
+    assertEquals(
+        analyses.left.getDeviceToOrderByExpressions(),
+        analyses.right.getDeviceToOrderByExpressions());
+    assertEquals(analyses.left.getDeviceToSortItems(), 
analyses.right.getDeviceToSortItems());
+  }
+
+  @Test
+  public void testNormalizedOrderByExpressionPreservedForNonAlignQueryRetry() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select s1 from root.sg.d1 order by Sin(s1)");
+
+    assertEquals(analyses.left.getOrderByExpressions(), 
analyses.right.getOrderByExpressions());
+    assertEquals(analyses.left.getMergeOrderParameter(), 
analyses.right.getMergeOrderParameter());
+  }
+
+  @Test
+  public void 
testGroupByControlExpressionPreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select count(s1) from root.sg.d1 group by 
variation(s1)");
+
+    assertEquals(analyses.left.getGroupByExpression(), 
analyses.right.getGroupByExpression());
+    assertEquals(
+        analyses.left.getAggregationExpressions(), 
analyses.right.getAggregationExpressions());
+  }
+
+  @Test
+  public void testMixedOrPredicatePreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice(
+            "select s1 from root.sg.d1 where "
+                + "(time > 1 and s1 > 1) or (time > 10 and s1 < 10)");
+
+    Assert.assertNotNull(analyses.left.getGlobalTimePredicate());
+    Assert.assertTrue(analyses.left.hasValueFilter());
+    assertEquals(analyses.left.getGlobalTimePredicate(), 
analyses.right.getGlobalTimePredicate());
+    assertEquals(analyses.left.getWhereExpression(), 
analyses.right.getWhereExpression());
+  }
+
+  @Test
+  public void testMixedNotPredicatePreservedWhenQueryAnalysisIsRetried() {
+    Pair<Analysis, Analysis> analyses =
+        analyzeSQLTwice("select s1 from root.sg.d1 where not(time > 1 and s1 > 
1)");
+
+    Assert.assertNull(analyses.left.getGlobalTimePredicate());
+    Assert.assertTrue(analyses.left.hasValueFilter());
+    assertEquals(analyses.left.getWhereExpression(), 
analyses.right.getWhereExpression());
+  }
+
+  @Test
+  public void testShowTimeSeriesTimeFilterPreservedWhenAnalysisIsRetried() {
+    assertMetadataTimeFilterPreservedWhenAnalysisIsRetried(
+        "show timeseries root.sg.** where time > 1 and time < 3");
+  }
+
+  @Test
+  public void testShowDevicesTimeFilterPreservedWhenAnalysisIsRetried() {
+    assertMetadataTimeFilterPreservedWhenAnalysisIsRetried(
+        "show devices root.sg.** where time > 1 and time < 3");
+  }
+
+  @Test
+  public void testCountTimeSeriesTimeFilterPreservedWhenAnalysisIsRetried() {
+    assertMetadataTimeFilterPreservedWhenAnalysisIsRetried(
+        "count timeseries root.sg.** where time > 1 and time < 3");
+  }
+
+  @Test
+  public void testCountDevicesTimeFilterPreservedWhenAnalysisIsRetried() {
+    assertMetadataTimeFilterPreservedWhenAnalysisIsRetried(
+        "count devices root.sg.** where time > 1 and time < 3");
+  }
+
   @Test
   public void testDataPartitionAnalyze() {
     Analysis analysis = analyzeSQL("insert into root.sg.d1(timestamp,s) 
values(1,10),(86401,11)");
@@ -1070,6 +1283,31 @@ public class AnalyzeTest {
     return null;
   }
 
+  private Pair<Analysis, Analysis> analyzeSQLTwice(String sql) {
+    Statement statement = StatementGenerator.createStatement(sql, 
ZonedDateTime.now().getOffset());
+    MPPQueryContext context =
+        new MPPQueryContext(
+            "",
+            new QueryId("test_query_retry"),
+            new SessionInfo(0, "test", ZoneId.systemDefault()),
+            new TEndPoint(),
+            new TEndPoint());
+    Analyzer analyzer =
+        new Analyzer(context, new FakePartitionFetcherImpl(), new 
FakeSchemaFetcherImpl());
+
+    Analysis initialAnalysis = analyzer.analyze(statement);
+    context.prepareForRetry();
+    Analysis retriedAnalysis = analyzer.analyze(statement);
+    return new Pair<>(initialAnalysis, retriedAnalysis);
+  }
+
+  private void assertMetadataTimeFilterPreservedWhenAnalysisIsRetried(String 
sql) {
+    Pair<Analysis, Analysis> analyses = analyzeSQLTwice(sql);
+
+    Assert.assertNotNull(analyses.left.getGlobalTimePredicate());
+    assertEquals(analyses.left.getGlobalTimePredicate(), 
analyses.right.getGlobalTimePredicate());
+  }
+
   private void alignByTimeAnalysisEqualTest(Analysis actualAnalysis, Analysis 
expectedAnalysis) {
     commonAnalysisEqualTest(expectedAnalysis, actualAnalysis);
     assertEquals(expectedAnalysis.getSourceExpressions(), 
actualAnalysis.getSourceExpressions());
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
new file mode 100644
index 00000000000..bc9d252d30e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.queryengine.plan.analyze;
+
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import 
org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils.GlobalTimePredicateExtractionResult;
+import org.apache.iotdb.db.queryengine.plan.expression.Expression;
+import org.apache.iotdb.db.queryengine.plan.expression.binary.BinaryExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
+import 
org.apache.iotdb.db.queryengine.plan.expression.multi.FunctionExpression;
+import 
org.apache.iotdb.db.queryengine.plan.expression.unary.LogicNotExpression;
+
+import org.junit.Test;
+
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.and;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.function;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.gt;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.intValue;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.longValue;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.lt;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.not;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.or;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.time;
+import static 
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.timeSeries;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class PredicateUtilsTest {
+
+  @Test
+  public void testExtractTimePredicateFromAndWithoutMutatingInput() throws 
IllegalPathException {
+    Expression timePredicate = gt(time(), longValue(1));
+    Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+    BinaryExpression predicate = and(timePredicate, valuePredicate);
+    String originalExpressionString = predicate.getExpressionString();
+
+    GlobalTimePredicateExtractionResult result =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+
+    assertSame(timePredicate, result.getGlobalTimePredicate());
+    assertSame(valuePredicate, result.getResidualPredicate());
+    assertTrue(result.hasValueFilter());
+    assertSame(timePredicate, predicate.getLeftExpression());
+    assertSame(valuePredicate, predicate.getRightExpression());
+    assertEquals(originalExpressionString, predicate.getExpressionString());
+  }
+
+  @Test
+  public void testExtractPureTimeOrWithoutMutatingInput() {
+    Expression leftTimePredicate = gt(time(), longValue(1));
+    Expression rightTimePredicate = lt(time(), longValue(10));
+    BinaryExpression predicate = or(leftTimePredicate, rightTimePredicate);
+
+    GlobalTimePredicateExtractionResult result =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+
+    assertEquals(predicate, result.getGlobalTimePredicate());
+    assertSame(ConstantOperand.TRUE, result.getResidualPredicate());
+    assertFalse(result.hasValueFilter());
+    assertSame(leftTimePredicate, predicate.getLeftExpression());
+    assertSame(rightTimePredicate, predicate.getRightExpression());
+  }
+
+  @Test
+  public void testMixedOrKeepsOriginalPredicateAsResidual() throws 
IllegalPathException {
+    Expression leftTimePredicate = gt(time(), longValue(1));
+    Expression rightTimePredicate = gt(time(), longValue(10));
+    Expression leftValuePredicate = gt(timeSeries("root.sg.d.s1"), 
intValue("1"));
+    Expression rightValuePredicate = gt(timeSeries("root.sg.d.s2"), 
intValue("2"));
+    BinaryExpression left = and(leftTimePredicate, leftValuePredicate);
+    BinaryExpression right = and(rightTimePredicate, rightValuePredicate);
+    BinaryExpression predicate = or(left, right);
+
+    GlobalTimePredicateExtractionResult result =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+
+    assertEquals(or(leftTimePredicate, rightTimePredicate), 
result.getGlobalTimePredicate());
+    assertSame(predicate, result.getResidualPredicate());
+    assertTrue(result.hasValueFilter());
+    assertSame(left, predicate.getLeftExpression());
+    assertSame(right, predicate.getRightExpression());
+  }
+
+  @Test
+  public void testMixedNotDoesNotExtractTimePredicate() throws 
IllegalPathException {
+    Expression timePredicate = gt(time(), longValue(1));
+    Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+    BinaryExpression child = and(timePredicate, valuePredicate);
+    LogicNotExpression predicate = not(child);
+
+    GlobalTimePredicateExtractionResult result =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+
+    assertNull(result.getGlobalTimePredicate());
+    assertSame(predicate, result.getResidualPredicate());
+    assertTrue(result.hasValueFilter());
+    assertSame(child, predicate.getExpression());
+  }
+
+  @Test
+  public void testPureTimeNotCanBeExtracted() {
+    Expression timePredicate = gt(time(), longValue(1));
+    LogicNotExpression predicate = not(timePredicate);
+
+    GlobalTimePredicateExtractionResult result =
+        PredicateUtils.extractGlobalTimePredicate(predicate);
+
+    assertEquals(predicate, result.getGlobalTimePredicate());
+    assertSame(ConstantOperand.TRUE, result.getResidualPredicate());
+    assertFalse(result.hasValueFilter());
+    assertSame(timePredicate, predicate.getExpression());
+  }
+
+  @Test
+  public void testSimplifierUsesCopyOnWriteForUnaryExpression() throws 
IllegalPathException {
+    Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+    BinaryExpression reducibleChild = and(ConstantOperand.TRUE, 
valuePredicate);
+    LogicNotExpression predicate = not(reducibleChild);
+
+    Expression simplified = PredicateUtils.simplifyPredicate(predicate);
+
+    assertNotSame(predicate, simplified);
+    assertTrue(simplified instanceof LogicNotExpression);
+    assertSame(valuePredicate, ((LogicNotExpression) 
simplified).getExpression());
+    assertSame(reducibleChild, predicate.getExpression());
+    assertSame(ConstantOperand.TRUE, reducibleChild.getLeftExpression());
+    assertSame(valuePredicate, reducibleChild.getRightExpression());
+  }
+
+  @Test
+  public void testSimplifierUsesCopyOnWriteForFunctionExpression() throws 
IllegalPathException {
+    Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+    BinaryExpression reducibleChild = and(ConstantOperand.TRUE, 
valuePredicate);
+    FunctionExpression functionExpression = function("test", reducibleChild);
+
+    Expression simplified = 
PredicateUtils.simplifyPredicate(functionExpression);
+
+    assertNotSame(functionExpression, simplified);
+    assertTrue(simplified instanceof FunctionExpression);
+    assertSame(valuePredicate, ((FunctionExpression) 
simplified).getExpressions().get(0));
+    assertSame(reducibleChild, functionExpression.getExpressions().get(0));
+    assertSame(ConstantOperand.TRUE, reducibleChild.getLeftExpression());
+  }
+
+  @Test
+  public void testSimplifierReusesUnchangedExpression() throws 
IllegalPathException {
+    Expression predicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+
+    assertSame(predicate, PredicateUtils.simplifyPredicate(predicate));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
new file mode 100644
index 00000000000..aea763bf291
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.queryengine.plan.analyze;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.commons.schema.template.Template;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import 
org.apache.iotdb.db.queryengine.plan.expression.multi.FunctionExpression;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+public class TemplatedAnalyzeRetryTest {
+
+  @Test
+  public void testCountTimeTemplateAnalysisDoesNotMutateStatementForRetry() 
throws Exception {
+    QueryStatement queryStatement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "select count_time(*) from root.sg.* align by device",
+                ZonedDateTime.now().getOffset());
+    queryStatement.semanticCheck();
+
+    Analysis analysis = new Analysis();
+    boolean canBuildPlan =
+        TemplatedAggregationAnalyze.canBuildAggregationPlanUseTemplate(
+            analysis,
+            queryStatement,
+            null,
+            null,
+            createQueryContext(),
+            createAlignedTemplate(),
+            Collections.emptyList());
+
+    assertTrue(canBuildPlan);
+    FunctionExpression countTimeExpression =
+        (FunctionExpression)
+            
queryStatement.getSelectComponent().getResultColumns().get(0).getExpression();
+    assertEquals("*", 
countTimeExpression.getExpressions().get(0).getOutputSymbol());
+    FunctionExpression analyzedCountTimeExpression =
+        (FunctionExpression) 
analysis.getAggregationExpressions().iterator().next();
+    assertNotSame(countTimeExpression, analyzedCountTimeExpression);
+    assertEquals("count_time(*)", 
analyzedCountTimeExpression.getExpressionString());
+    assertEquals("count_time(Time)", 
analyzedCountTimeExpression.getOutputSymbol());
+
+    // QueryExecution analyzes the same statement again when retrying a 
dispatch failure.
+    queryStatement.semanticCheck();
+  }
+
+  @Test
+  public void testFailedTemplateAttemptDoesNotConsumeLimitOffset() throws 
Exception {
+    QueryStatement queryStatement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "select __endTime, count(s1) from root.sg.* "
+                    + "group by ([0, 100), 10ms) limit 5 offset 12 align by 
device",
+                ZonedDateTime.now().getOffset());
+
+    boolean canBuildPlan =
+        TemplatedAggregationAnalyze.canBuildAggregationPlanUseTemplate(
+            new Analysis(),
+            queryStatement,
+            null,
+            null,
+            createQueryContext(),
+            createAlignedTemplate(),
+            Arrays.asList(new PartialPath("root.sg.d1"), new 
PartialPath("root.sg.d2")));
+
+    assertFalse(canBuildPlan);
+    assertEquals(
+        Arrays.asList(5L, 12L, 0L, 100L),
+        Arrays.asList(
+            queryStatement.getRowLimit(),
+            queryStatement.getRowOffset(),
+            queryStatement.getGroupByTimeComponent().getStartTime(),
+            queryStatement.getGroupByTimeComponent().getEndTime()));
+  }
+
+  private static MPPQueryContext createQueryContext() {
+    return new MPPQueryContext(
+        "",
+        new QueryId("test_query"),
+        new SessionInfo(0, "test", ZoneId.systemDefault()),
+        new TEndPoint(),
+        new TEndPoint());
+  }
+
+  private static Template createAlignedTemplate() throws Exception {
+    return new Template(
+        "template",
+        Collections.emptyList(),
+        Collections.emptyList(),
+        Collections.emptyList(),
+        Collections.emptyList(),
+        true);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
new file mode 100644
index 00000000000..1d717d0f7d9
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.queryengine.plan.execution;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.execution.QueryStateMachine;
+import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
+import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
+import org.apache.iotdb.db.queryengine.plan.planner.IPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.scheduler.IScheduler;
+import org.apache.iotdb.rpc.RpcUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class QueryExecutionRetryTest {
+
+  private ExecutorService stateMachineExecutor;
+
+  @Before
+  public void setUp() {
+    stateMachineExecutor = Executors.newSingleThreadExecutor();
+  }
+
+  @After
+  public void tearDown() {
+    stateMachineExecutor.shutdownNow();
+  }
+
+  @Test
+  public void testAnalysisAttemptIsRolledBackOnlyWhenRetryStarts() {
+    RecordingPlanner planner = new RecordingPlanner(false);
+    QueryExecution execution =
+        new QueryExecution(planner, createQueryContext("retry_succeeds"), 
stateMachineExecutor);
+
+    assertEquals(Arrays.asList("begin", "analyze"), 
planner.getAnalysisEvents());
+
+    execution.start();
+
+    assertEquals(1, planner.getScheduleAttempts());
+    // PENDING_RETRY must keep the journal alive until the plans from this 
attempt have been
+    // stopped. In particular, returning from start() must not commit or roll 
back the attempt.
+    assertEquals(Arrays.asList("begin", "analyze"), 
planner.getAnalysisEvents());
+
+    ExecutionResult result = execution.getStatus();
+
+    assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
result.status.code);
+    assertEquals(2, planner.getScheduleAttempts());
+    assertEquals(
+        Arrays.asList("begin", "analyze", "rollback", "begin", "analyze", 
"commit"),
+        planner.getAnalysisEvents());
+  }
+
+  @Test
+  public void testAnalysisAttemptIsRolledBackWhenRetryLimitIsReached() throws 
Exception {
+    RecordingPlanner planner = new RecordingPlanner(true);
+    QueryExecution execution =
+        new QueryExecution(planner, createQueryContext("retry_exhausted"), 
stateMachineExecutor);
+
+    execution.start();
+    assertEquals(Arrays.asList("begin", "analyze"), 
planner.getAnalysisEvents());
+
+    setRetryCount(execution, Integer.MAX_VALUE);
+    ExecutionResult result = execution.getStatus();
+
+    assertEquals(TSStatusCode.DISPATCH_ERROR.getStatusCode(), 
result.status.code);
+    assertEquals(Arrays.asList("begin", "analyze", "rollback"), 
planner.getAnalysisEvents());
+    assertFalse(planner.getAnalysisEvents().contains("commit"));
+  }
+
+  private static MPPQueryContext createQueryContext(String queryId) {
+    MPPQueryContext context =
+        new MPPQueryContext(
+            "",
+            new QueryId(queryId),
+            new SessionInfo(0, "test", ZoneId.systemDefault()),
+            new TEndPoint(),
+            new TEndPoint());
+    // Keeping this as a non-query avoids constructing a result exchange 
handle. The fake scheduler
+    // still drives the same QueryStateMachine transitions used by a retryable 
query dispatch.
+    context.setQueryType(QueryType.OTHER);
+    return context;
+  }
+
+  private static void setRetryCount(QueryExecution execution, int retryCount) 
throws Exception {
+    Field retryCountField = 
QueryExecution.class.getDeclaredField("retryCount");
+    retryCountField.setAccessible(true);
+    retryCountField.setInt(execution, retryCount);
+  }
+
+  private static class RecordingPlanner implements IPlanner {
+
+    private final IAnalysis analysis = mock(IAnalysis.class);
+    private final IScheduler scheduler = mock(IScheduler.class);
+    private final LogicalQueryPlan logicalPlan = mock(LogicalQueryPlan.class);
+    private final DistributedQueryPlan distributedPlan = 
mock(DistributedQueryPlan.class);
+    private final List<String> analysisEvents = new ArrayList<>();
+    private final boolean alwaysFailDispatch;
+
+    private int scheduleAttempts;
+
+    private RecordingPlanner(boolean alwaysFailDispatch) {
+      this.alwaysFailDispatch = alwaysFailDispatch;
+      when(distributedPlan.getInstances()).thenReturn(Collections.emptyList());
+    }
+
+    @Override
+    public void beginAnalysisAttempt() {
+      analysisEvents.add("begin");
+    }
+
+    @Override
+    public IAnalysis analyze(MPPQueryContext context) {
+      analysisEvents.add("analyze");
+      return analysis;
+    }
+
+    @Override
+    public void rollbackAnalysisAttempt() {
+      analysisEvents.add("rollback");
+    }
+
+    @Override
+    public void commitAnalysisAttempt() {
+      analysisEvents.add("commit");
+    }
+
+    @Override
+    public LogicalQueryPlan doLogicalPlan(IAnalysis analysis, MPPQueryContext 
context) {
+      return logicalPlan;
+    }
+
+    @Override
+    public DistributedQueryPlan doDistributionPlan(
+        IAnalysis analysis, LogicalQueryPlan logicalPlan, MPPQueryContext 
context) {
+      return distributedPlan;
+    }
+
+    @Override
+    public IScheduler doSchedule(
+        IAnalysis analysis,
+        DistributedQueryPlan distributedPlan,
+        MPPQueryContext context,
+        QueryStateMachine stateMachine) {
+      scheduleAttempts++;
+      stateMachine.transitionToDispatching();
+      if (alwaysFailDispatch || scheduleAttempts == 1) {
+        TSStatus dispatchFailure = 
RpcUtils.getStatus(TSStatusCode.DISPATCH_ERROR);
+        stateMachine.transitionToPendingRetry(dispatchFailure);
+      } else {
+        stateMachine.transitionToRunning();
+      }
+      return scheduler;
+    }
+
+    @Override
+    public void invalidatePartitionCache() {
+      // No cache is used by this fake planner.
+    }
+
+    @Override
+    public ScheduledExecutorService getScheduledExecutorService() {
+      return null;
+    }
+
+    @Override
+    public void setRedirectInfo(IAnalysis analysis, TEndPoint localEndPoint, 
TSStatus status) {
+      // Redirect information is irrelevant to the retry journal lifecycle.
+    }
+
+    private List<String> getAnalysisEvents() {
+      return analysisEvents;
+    }
+
+    private int getScheduleAttempts() {
+      return scheduleAttempts;
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
new file mode 100644
index 00000000000..07e7baa99fc
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.queryengine.plan.planner;
+
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.SortNode;
+import org.apache.iotdb.db.queryengine.plan.statement.component.OrderByKey;
+import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
+import org.apache.iotdb.db.queryengine.plan.statement.component.SortItem;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+
+import org.junit.Test;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class LogicalPlanBuilderTest {
+
+  @Test
+  public void testPlanDeviceViewDoesNotAppendDefaultSortItemsToStatement() {
+    QueryStatement queryStatement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "select s1 from root.sg.* order by time desc align by device",
+                ZonedDateTime.now().getOffset());
+    int originalSortItemCount = queryStatement.getSortItemList().size();
+
+    planDeviceView(queryStatement, "first_plan");
+    int sortItemCountAfterFirstPlan = queryStatement.getSortItemList().size();
+    planDeviceView(queryStatement, "retry_plan");
+
+    assertEquals(
+        Arrays.asList(originalSortItemCount, originalSortItemCount),
+        Arrays.asList(sortItemCountAfterFirstPlan, 
queryStatement.getSortItemList().size()));
+  }
+
+  @Test
+  public void testPlanOrderByUsesDeviceViewMergeKeysWithoutMutatingStatement() 
{
+    QueryStatement queryStatement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "select s1 from root.sg.* order by s1 desc align by device",
+                ZonedDateTime.now().getOffset());
+    List<SortItem> originalSortItems = new 
ArrayList<>(queryStatement.getSortItemList());
+    String originalOrderByClause = 
queryStatement.getOrderByComponent().toSQLString();
+    List<SortItem> expectedSortItems = new ArrayList<>(originalSortItems);
+    expectedSortItems.add(new SortItem(OrderByKey.DEVICE, Ordering.ASC));
+    expectedSortItems.add(new SortItem(OrderByKey.TIME, Ordering.ASC));
+
+    Analysis analysis = new Analysis();
+    analysis.setOrderByExpressions(Collections.emptySet());
+    analysis.setSelectExpressions(Collections.emptySet());
+    LogicalPlanBuilder builder =
+        new LogicalPlanBuilder(analysis, new MPPQueryContext(new 
QueryId("two_stage_plan")))
+            .planDeviceView(
+                Collections.emptyMap(),
+                Collections.emptySet(),
+                Collections.emptyMap(),
+                Collections.emptySet(),
+                queryStatement,
+                analysis)
+            .planOrderBy(queryStatement, analysis);
+
+    assertTrue(builder.getRoot() instanceof SortNode);
+    assertTrue(
+        ((SortNode) builder.getRoot())
+            .getOrderByParameter()
+            .getSortItemList()
+            .get(0)
+            .isExpression());
+    assertEquals(
+        expectedSortItems, ((SortNode) 
builder.getRoot()).getOrderByParameter().getSortItemList());
+    assertEquals(originalSortItems, queryStatement.getSortItemList());
+    assertEquals(originalOrderByClause, 
queryStatement.getOrderByComponent().toSQLString());
+  }
+
+  private static void planDeviceView(QueryStatement queryStatement, String 
queryId) {
+    Analysis analysis = new Analysis();
+    new LogicalPlanBuilder(analysis, new MPPQueryContext(new QueryId(queryId)))
+        .planDeviceView(
+            Collections.emptyMap(),
+            Collections.emptySet(),
+            Collections.emptyMap(),
+            Collections.emptySet(),
+            queryStatement,
+            analysis);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
index 107070425e5..5b5a384f756 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
@@ -47,8 +47,11 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.LogicalExpression;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.type.InternalTypeManager;
+import org.apache.iotdb.commons.schema.filter.SchemaFilter;
 import org.apache.iotdb.commons.schema.table.InsertNodeMeasurementInfo;
 import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TagColumnSchema;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.protocol.session.InternalClientSession;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
@@ -73,6 +76,8 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableS
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode;
 import org.apache.iotdb.db.queryengine.plan.relational.security.AccessControl;
 import 
org.apache.iotdb.db.queryengine.plan.relational.security.AllowAllAccessControl;
+import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AbstractQueryDeviceWithCache;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ShowDevice;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.rewrite.StatementRewriteFactory;
 import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
@@ -81,6 +86,8 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement
 import org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
 
 import com.google.common.collect.ImmutableSet;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.file.metadata.IDeviceID.Factory;
 import org.apache.tsfile.utils.Binary;
 import org.junit.BeforeClass;
@@ -127,7 +134,9 @@ import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 
@@ -1103,6 +1112,114 @@ public class AnalyzerTest {
         
analysis.getRespDatasetHeader().getColumnNameIndexMap().get("TAG1").intValue());
   }
 
+  @Test
+  public void testShowDeviceWithWhereCanBeAnalyzedAgain() {
+    assertDeviceQueryCanBeAnalyzedAgain("SHOW DEVICES FROM table1 WHERE tag1 = 
'shanghai'");
+  }
+
+  @Test
+  public void testCountDeviceWithWhereCanBeAnalyzedAgain() {
+    assertDeviceQueryCanBeAnalyzedAgain("COUNT DEVICES FROM table1 WHERE tag1 
= 'shanghai'");
+  }
+
+  @Test
+  public void testInternalShowDeviceKeepsPreparedTraversalState() {
+    final String database = "testdb";
+    final TsTable tsTable = new TsTable(table);
+    tsTable.addColumnSchema(new TagColumnSchema("tag1", TSDataType.STRING));
+    tsTable.addColumnSchema(new AttributeColumnSchema("attr1", 
TSDataType.STRING));
+    DataNodeTableCache.getInstance().preUpdateTable(database, tsTable, null);
+    DataNodeTableCache.getInstance().commitUpdateTable(database, table, null);
+
+    try {
+      final ShowDevice statement = new ShowDevice(database, table);
+      final List<List<SchemaFilter>> tagFilters =
+          Collections.singletonList(Collections.emptyList());
+      final List<IDeviceID> partitionKeys =
+          Collections.singletonList(
+              Factory.DEFAULT_FACTORY.create(new String[] {table, 
"shanghai"}));
+      final List<String> attributeColumns = Collections.singletonList("attr1");
+      statement.setTagDeterminedFilterList(tagFilters);
+      statement.setPartitionKeyList(partitionKeys);
+      statement.setAttributeColumns(attributeColumns);
+
+      final SessionInfo session =
+          new SessionInfo(0, "test", ZoneId.systemDefault(), database, 
SqlDialect.TABLE);
+      final MPPQueryContext queryContext =
+          new MPPQueryContext("", new QueryId("internal_show_device"), 
session, null, null);
+      final Analysis analysis =
+          analyzeStatement(statement, TEST_MATADATA, queryContext, new 
SqlParser(), session);
+
+      final ShowDevice analyzedStatement = (ShowDevice) 
analysis.getStatement();
+      assertSame(statement, analyzedStatement);
+      assertSame(tagFilters, analyzedStatement.getTagDeterminedFilterList());
+      assertSame(partitionKeys, analyzedStatement.getPartitionKeyList());
+      assertSame(attributeColumns, analyzedStatement.getAttributeColumns());
+    } finally {
+      DataNodeTableCache.getInstance().invalid(database);
+    }
+  }
+
+  private void assertDeviceQueryCanBeAnalyzedAgain(final String sql) {
+    final String database = "testdb";
+    final TsTable tsTable = new TsTable(table);
+    tsTable.addColumnSchema(new TagColumnSchema("tag1", TSDataType.STRING));
+    tsTable.addColumnSchema(new TagColumnSchema("tag2", TSDataType.STRING));
+    tsTable.addColumnSchema(new TagColumnSchema("tag3", TSDataType.STRING));
+    tsTable.addColumnSchema(new AttributeColumnSchema("attr1", 
TSDataType.STRING));
+    tsTable.addColumnSchema(new AttributeColumnSchema("attr2", 
TSDataType.STRING));
+    DataNodeTableCache.getInstance().preUpdateTable(database, tsTable, null);
+    DataNodeTableCache.getInstance().commitUpdateTable(database, table, null);
+
+    try {
+      final SqlParser sqlParser = new SqlParser();
+      final Statement statement =
+          sqlParser.createStatement(
+              sql, ZoneId.systemDefault(), new 
InternalClientSession("testClient"));
+      final SessionInfo session =
+          new SessionInfo(0, "test", ZoneId.systemDefault(), database, 
SqlDialect.TABLE);
+      final MPPQueryContext queryContext =
+          new MPPQueryContext(sql, new QueryId("device_query_retry"), session, 
null, null);
+
+      final AbstractQueryDeviceWithCache parsedStatement = 
(AbstractQueryDeviceWithCache) statement;
+      final Expression parsedWhere = 
parsedStatement.getWhere().orElseThrow(AssertionError::new);
+      final String parsedStatementText = parsedStatement.toString();
+      final long parsedStatementSize = parsedStatement.ramBytesUsed();
+
+      final Analysis firstAnalysis =
+          analyzeStatement(statement, TEST_MATADATA, queryContext, sqlParser, 
session);
+      assertParsedDeviceStatementUnchanged(
+          parsedStatement, parsedWhere, parsedStatementText, 
parsedStatementSize);
+      assertNotSame(statement, firstAnalysis.getStatement());
+
+      queryContext.prepareForRetry();
+      final Analysis retryAnalysis =
+          analyzeStatement(statement, TEST_MATADATA, queryContext, sqlParser, 
session);
+      assertParsedDeviceStatementUnchanged(
+          parsedStatement, parsedWhere, parsedStatementText, 
parsedStatementSize);
+      assertNotSame(statement, retryAnalysis.getStatement());
+      assertNotSame(firstAnalysis.getStatement(), 
retryAnalysis.getStatement());
+    } finally {
+      DataNodeTableCache.getInstance().invalid(database);
+    }
+  }
+
+  private void assertParsedDeviceStatementUnchanged(
+      final AbstractQueryDeviceWithCache parsedStatement,
+      final Expression parsedWhere,
+      final String parsedStatementText,
+      final long parsedStatementSize) {
+    assertSame(parsedWhere, 
parsedStatement.getWhere().orElseThrow(AssertionError::new));
+    assertEquals(parsedStatementText, parsedStatement.toString());
+    assertEquals(parsedStatementSize, parsedStatement.ramBytesUsed());
+    assertNull(parsedStatement.getDatabase());
+    assertNull(parsedStatement.getTableName());
+    assertNull(parsedStatement.getTagFuzzyPredicate());
+    assertNull(parsedStatement.getPartitionKeyList());
+    assertNull(parsedStatement.getAttributeColumns());
+    assertNull(parsedStatement.getColumnHeaderList());
+  }
+
   private Metadata mockMetadataForInsertion() {
     final TsTable tsTable = StatementTestUtils.genTsTable();
     DataNodeTableCache.getInstance().preUpdateTable(database, tsTable, null);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/QueryStatementTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/QueryStatementTest.java
index ad812e74a0c..7fec82a77af 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/QueryStatementTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/QueryStatementTest.java
@@ -21,7 +21,9 @@ package org.apache.iotdb.db.queryengine.plan.statement;
 
 import org.apache.iotdb.commons.exception.SemanticException;
 import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.db.queryengine.plan.expression.Expression;
 import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import 
org.apache.iotdb.db.queryengine.plan.statement.component.OrderByComponent;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
 
 import org.apache.tsfile.utils.Pair;
@@ -35,6 +37,8 @@ import java.util.List;
 
 import static 
org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement.RAW_AGGREGATION_HYBRID_QUERY_ERROR_MSG;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
 public class QueryStatementTest {
@@ -155,6 +159,28 @@ public class QueryStatementTest {
     assertEquals("root.sg.d1.s3", fullPaths.get(1).getFullPath());
   }
 
+  @Test
+  public void testOrderByCopyPreservesRawAndNormalizedExpressions() {
+    QueryStatement statement =
+        (QueryStatement)
+            StatementGenerator.createStatement(
+                "SELECT s1 FROM root.sg.d1 ORDER BY Sin(s1)", 
ZonedDateTime.now().getOffset());
+    OrderByComponent source = statement.getOrderByComponent();
+    Expression rawExpression = source.getSortItemList().get(0).getExpression();
+    Expression normalizedExpression = 
source.getExpressionSortItemList().get(0);
+
+    assertEquals("Sin(s1)", rawExpression.getExpressionString());
+    assertEquals("sin(s1)", normalizedExpression.getExpressionString());
+    assertNotSame(rawExpression, normalizedExpression);
+
+    OrderByComponent copy = OrderByComponent.copyOf(source);
+
+    assertNotSame(source.getSortItemList(), copy.getSortItemList());
+    assertNotSame(source.getSortItemList().get(0), 
copy.getSortItemList().get(0));
+    assertSame(rawExpression, copy.getSortItemList().get(0).getExpression());
+    assertSame(normalizedExpression, copy.getExpressionSortItemList().get(0));
+  }
+
   private void checkErrorQuerySql(String sql) {
     QueryStatement statement =
         (QueryStatement) StatementGenerator.createStatement(sql, 
ZonedDateTime.now().getOffset());

Reply via email to