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

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


The following commit(s) were added to refs/heads/master by this push:
     new 47611dceac3 [Fix](Query Stats) Add QueryStatsRecorder for column-level 
query and filter - Part2 (#63768)
47611dceac3 is described below

commit 47611dceac33814d9df7e80ba85f32dfa8909d18
Author: nsivarajan <[email protected]>
AuthorDate: Mon Jun 1 15:55:20 2026 +0530

    [Fix](Query Stats) Add QueryStatsRecorder for column-level query and filter 
- Part2 (#63768)
    
    ### What problem does this PR solve?
    
    Related PR: #63067
    
    Problem Summary:
    
    PR is a Follow-up of #63067 , Extends column-level query/filter hit
    recording to cover all major Nereids physical plan constructs beyond the
    base PhysicalOlapScan:
    
      - Alias resolution: SELECT k1 AS name records k1.queryHit
      - GROUP BY keys: GROUP BY k1 records k1.queryHit
      - Aggregate input columns: SUM(k2) records k2.queryHit
      - ORDER BY columns: ORDER BY k2 records k2.queryHit
      - Window PARTITION BY / ORDER BY keys
      - Window value columns: SUM(k2) OVER (...) records k2.queryHit
    - JOIN ON conditions (hash + non-equi): records filterHit on both sides
      - ROLLUP/CUBE grouping sets via PhysicalRepeat
      - PartitionTopN partition and order keys (ROW_NUMBER per-partition)
    - Storage-layer aggregate pushdown: COUNT(*)/MIN/MAX queries record
    stats
      - Lazy materialization scan slot remapping via row-id lookup
    
    Out of scope (tracked for Part 3):
    
    The following cases are intentionally deferred and not bugs in this PR:
    - UNION / INTERSECT / EXCEPT — set operation output slots are not yet
    remapped to child scans
    - CTE consumer columns — consumer-side slot IDs differ from producer
    scan slots
      - LATERAL VIEW / EXPLODE — generator output slots are not yet remapped
    - HAVING SUM(k2) > 0 — aggregate output predicates; simple HAVING k1 > 0
    already works
    - External tables (Hive / Iceberg / JDBC) — deferred, requires separate
    design
    
    ---------
    
    Co-authored-by: Sivarajan Narayanan <[email protected]>
---
 .../doris/statistics/query/QueryStatsRecorder.java | 290 ++++++++++--
 .../doris/statistics/query/QueryStatsUtil.java     |   5 +-
 .../statistics/query/QueryStatsRecorderTest.java   | 518 ++++++++++++++++++++-
 .../suites/query_p0/stats/query_stats_test.groovy  |  50 +-
 4 files changed, 828 insertions(+), 35 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java
index 1b5b76a1bc5..78ce44dc3af 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java
@@ -24,39 +24,52 @@ import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.common.Config;
 import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.glue.LogicalPlanAdapter;
+import org.apache.doris.nereids.properties.OrderKey;
+import 
org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup;
+import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.OrderExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.WindowExpression;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.Aggregate;
+import org.apache.doris.nereids.trees.plans.algebra.Relation;
 import org.apache.doris.nereids.trees.plans.commands.Command;
+import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin;
+import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalSort;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterialize;
 import 
org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterializeOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
+import 
org.apache.doris.nereids.trees.plans.physical.PhysicalStorageLayerAggregate;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalWindow;
 import org.apache.doris.qe.ConnectContext;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Records column-level query-hit and filter-hit statistics from the Nereids 
physical plan.
  * Called once per query in NereidsPlanner after plan translation.
  *
- * <p>Scope (Part 1):
- * <ul>
- *   <li>queryHit: base SELECT columns whose ExprId flows straight through to 
the root
- *       plan's output without rewriting. Columns hidden by an alias, an 
expression,
- *       or an aggregate function are NOT recorded yet (Part 2).</li>
- *   <li>filterHit: columns referenced in WHERE predicate conjuncts.</li>
- *   <li>Only OlapTable scans are recorded; external tables (Hive, Iceberg, 
JDBC, …) are not.</li>
- *   <li>DML, EXPLAIN, and internal queries (e.g. auto-analyze) are 
skipped.</li>
- *   <li>Per query, each table's count is incremented at most once regardless 
of scan count.</li>
- * </ul>
- * GROUP BY, ORDER BY, window, JOIN, and aliased/projected columns are 
deferred to Part 2.
+ * <p>queryHit: SELECT output columns (alias-unwrapped), GROUP BY keys,
+ * ORDER BY keys, window PARTITION BY / ORDER BY keys, aggregate input columns.
+ * filterHit: WHERE predicate columns and JOIN ON conditions.
+ * Only OlapTable scans are recorded. DML, EXPLAIN, and internal queries are 
skipped.
+ * Per query each table's count is incremented at most once regardless of scan 
count.
  */
 public class QueryStatsRecorder {
     private static final Logger LOG = 
LogManager.getLogger(QueryStatsRecorder.class);
@@ -100,23 +113,32 @@ public class QueryStatsRecorder {
      */
     static Map<String, StatsDelta> collectDeltas(PhysicalPlan plan) {
         Map<ExprId, PhysicalOlapScan> exprIdToScan = new HashMap<>();
+        Map<ExprId, String> exprIdToColName = new HashMap<>();
         Map<String, StatsDelta> deltas = new HashMap<>();
-        walkPlan(plan, exprIdToScan, deltas);
+        walkPlan(plan, exprIdToScan, exprIdToColName, deltas);
         if (exprIdToScan.isEmpty()) {
             return deltas;
         }
-        for (Slot slot : plan.getOutput()) {
-            if (!(slot instanceof SlotReference)) {
+        // queryHit: use getProjects() for PhysicalProject so Alias nodes are 
visible to unwrapAlias.
+        Iterable<? extends NamedExpression> rootExprs = (plan instanceof 
PhysicalProject)
+                ? ((PhysicalProject<?>) plan).getProjects()
+                : plan.getOutput();
+        for (NamedExpression ne : rootExprs) {
+            SlotReference sr = unwrapAlias(ne);
+            if (sr == null) {
                 continue;
             }
-            SlotReference sr = (SlotReference) slot;
             PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
             if (sourceScan == null) {
                 continue;
             }
             StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
             if (delta != null) {
-                sr.getOriginalColumn().ifPresent(col -> 
delta.addQueryStats(col.getName()));
+                String colName = sr.getOriginalColumn().map(col -> 
col.getName())
+                        .orElseGet(() -> exprIdToColName.get(sr.getExprId()));
+                if (colName != null) {
+                    delta.addQueryStats(colName);
+                }
             }
         }
         return deltas;
@@ -149,20 +171,38 @@ public class QueryStatsRecorder {
 
     /**
      * Single-pass tree walk: registers scan output slots into exprIdToScan,
-     * and records filterHit for PhysicalFilter conjuncts.
-     * Children are visited before the current node so scans are registered
-     * before parent filters look them up.
+     * records filterHit for WHERE conjuncts, and records queryHit for
+     * GROUP BY / ORDER BY / window keys and aggregate input columns.
+     * Children are visited before the current node so scans are registered 
first.
      * PhysicalLazyMaterializeOlapScan is checked before PhysicalOlapScan
      * because it is a subclass; the inner scan's metadata must be used.
      */
     private static void walkPlan(Plan plan,
             Map<ExprId, PhysicalOlapScan> exprIdToScan,
+            Map<ExprId, String> exprIdToColName,
             Map<String, StatsDelta> deltas) {
+        if (plan instanceof PhysicalStorageLayerAggregate) {
+            // COUNT(*)/MIN/MAX pushdown — the aggregate wraps the real scan 
but has no children.
+            PhysicalRelation inner = ((PhysicalStorageLayerAggregate) 
plan).getRelation();
+            if (inner instanceof PhysicalOlapScan) {
+                PhysicalOlapScan scan = (PhysicalOlapScan) inner;
+                for (Slot slot : scan.getOutput()) {
+                    exprIdToScan.put(slot.getExprId(), scan);
+                    if (slot instanceof SlotReference) {
+                        registerColName(exprIdToColName, slot.getExprId(), 
(SlotReference) slot);
+                    }
+                }
+            }
+            return;
+        }
         if (plan instanceof PhysicalLazyMaterializeOlapScan) {
             PhysicalOlapScan inner =
                     ((PhysicalLazyMaterializeOlapScan) plan).getScan();
             for (Slot slot : plan.getOutput()) {
                 exprIdToScan.put(slot.getExprId(), inner);
+                if (slot instanceof SlotReference) {
+                    registerColName(exprIdToColName, slot.getExprId(), 
(SlotReference) slot);
+                }
             }
             return;
         }
@@ -170,33 +210,221 @@ public class QueryStatsRecorder {
             PhysicalOlapScan scan = (PhysicalOlapScan) plan;
             for (Slot slot : scan.getOutput()) {
                 exprIdToScan.put(slot.getExprId(), scan);
+                if (slot instanceof SlotReference) {
+                    registerColName(exprIdToColName, slot.getExprId(), 
(SlotReference) slot);
+                }
             }
             return;
         }
+        // TODO: PhysicalCTEConsumer slots use consumer-side ExprIds that 
differ from the producer
+        // scan's ExprIds, so CTE column stats are silently missed. Fix 
requires mapping consumer
+        // slots back to producer slots via 
StatementContext.getConsumerToProducerSlotMap().
         for (Plan child : plan.children()) {
-            walkPlan(child, exprIdToScan, deltas);
+            walkPlan(child, exprIdToScan, exprIdToColName, deltas);
         }
         if (plan instanceof PhysicalFilter) {
             PhysicalFilter<?> filter = (PhysicalFilter<?>) plan;
             for (Expression conjunct : filter.getConjuncts()) {
-                conjunct.getInputSlots().forEach(slot -> {
-                    if (!(slot instanceof SlotReference)) {
-                        return;
+                recordInputSlotsAsFilterHit(conjunct, exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        // Lazy-materialized columns are not in 
PhysicalLazyMaterializeOlapScan.getOutput()
+        // (only operative slots + row-id are). The parent 
PhysicalLazyMaterialize exposes
+        // the lazy slots via getLazySlots(). Use each row-id — already 
registered by the
+        // child scan branch — to look up the source scan and register the 
lazy ExprIds.
+        if (plan instanceof PhysicalLazyMaterialize) {
+            PhysicalLazyMaterialize<?> lazy = (PhysicalLazyMaterialize<?>) 
plan;
+            List<Relation> rels = lazy.getRelations();
+            List<Slot> rowIds = lazy.getRowIds();
+            for (int i = 0; i < rels.size() && i < rowIds.size(); i++) {
+                PhysicalOlapScan sourceScan = 
exprIdToScan.get(rowIds.get(i).getExprId());
+                if (sourceScan == null) {
+                    continue;
+                }
+                for (Slot lazySlot : lazy.getLazySlots(rels.get(i))) {
+                    exprIdToScan.put(lazySlot.getExprId(), sourceScan);
+                    if (lazySlot instanceof SlotReference) {
+                        registerColName(exprIdToColName, lazySlot.getExprId(),
+                                (SlotReference) lazySlot);
                     }
-                    SlotReference sr = (SlotReference) slot;
-                    PhysicalOlapScan sourceScan = 
exprIdToScan.get(sr.getExprId());
-                    if (sourceScan == null) {
-                        return;
+                }
+            }
+        }
+        if (plan instanceof Aggregate) {
+            Aggregate<?> agg = (Aggregate<?>) plan;
+            // GROUP BY keys
+            for (Expression expr : agg.getGroupByExpressions()) {
+                recordInputSlotsAsQueryHit(expr, exprIdToScan, 
exprIdToColName, deltas);
+            }
+            // Columns consumed by aggregate functions (e.g. k2 in SUM(k2))
+            for (NamedExpression expr : agg.getOutputExpressions()) {
+                recordInputSlotsAsQueryHit(expr, exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        if (plan instanceof AbstractPhysicalSort) {
+            for (OrderKey orderKey : ((AbstractPhysicalSort<?>) 
plan).getOrderKeys()) {
+                recordInputSlotsAsQueryHit(orderKey.getExpr(), exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        // PhysicalPartitionTopN does not extend AbstractPhysicalSort but also 
has ORDER BY and
+        // partition keys (used for row_number() / rank() per partition).
+        if (plan instanceof PhysicalPartitionTopN) {
+            PhysicalPartitionTopN<?> ptn = (PhysicalPartitionTopN<?>) plan;
+            for (Expression partKey : ptn.getPartitionKeys()) {
+                recordInputSlotsAsQueryHit(partKey, exprIdToScan, 
exprIdToColName, deltas);
+            }
+            for (OrderKey orderKey : ptn.getOrderKeys()) {
+                recordInputSlotsAsQueryHit(orderKey.getExpr(), exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        // PhysicalRepeat handles ROLLUP/CUBE: group sets are like GROUP BY 
keys.
+        if (plan instanceof PhysicalRepeat) {
+            PhysicalRepeat<?> repeat = (PhysicalRepeat<?>) plan;
+            for (List<Expression> groupSet : repeat.getGroupingSets()) {
+                for (Expression expr : groupSet) {
+                    recordInputSlotsAsQueryHit(expr, exprIdToScan, 
exprIdToColName, deltas);
+                }
+            }
+            for (NamedExpression expr : repeat.getOutputExpressions()) {
+                recordInputSlotsAsQueryHit(expr, exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        if (plan instanceof PhysicalWindow) {
+            WindowFrameGroup wfg = ((PhysicalWindow<?>) 
plan).getWindowFrameGroup();
+            Set<Expression> partitionKeys = wfg.getPartitionKeys();
+            for (Expression expr : partitionKeys) {
+                recordInputSlotsAsQueryHit(expr, exprIdToScan, 
exprIdToColName, deltas);
+            }
+            for (OrderExpression orderExpr : wfg.getOrderKeys()) {
+                recordInputSlotsAsQueryHit(orderExpr.child(), exprIdToScan, 
exprIdToColName, deltas);
+            }
+            // queryHit for the window function value columns (e.g. k2 in 
SUM(k2) OVER (...)).
+            for (NamedExpression windowAlias : wfg.getGroups()) {
+                Expression windowExpr = windowAlias.child(0);
+                if (windowExpr instanceof WindowExpression) {
+                    recordInputSlotsAsQueryHit(
+                            ((WindowExpression) windowExpr).getFunction(),
+                            exprIdToScan, exprIdToColName, deltas);
+                }
+            }
+        }
+        // filterHit for JOIN ON conditions (hash equality and non-equality 
predicates).
+        if (plan instanceof AbstractPhysicalJoin) {
+            AbstractPhysicalJoin<?, ?> join = (AbstractPhysicalJoin<?, ?>) 
plan;
+            for (Expression conjunct : join.getHashJoinConjuncts()) {
+                recordInputSlotsAsFilterHit(conjunct, exprIdToScan, 
exprIdToColName, deltas);
+            }
+            for (Expression conjunct : join.getOtherJoinConjuncts()) {
+                recordInputSlotsAsFilterHit(conjunct, exprIdToScan, 
exprIdToColName, deltas);
+            }
+        }
+        // Propagate alias ExprIds for intermediate PhysicalProject nodes so 
that parent
+        // plan output slots (derived from aliases) resolve back to the 
original scan.
+        if (plan instanceof PhysicalProject) {
+            for (NamedExpression ne : ((PhysicalProject<?>) 
plan).getProjects()) {
+                if (exprIdToScan.containsKey(ne.getExprId())) {
+                    continue; // plain slot pass-through — already registered 
by child scan
+                }
+                SlotReference underlying = unwrapAlias(ne);
+                if (underlying != null && 
!underlying.getExprId().equals(ne.getExprId())) {
+                    // Simple alias: Alias(SlotRef) — propagate scan and 
column name.
+                    PhysicalOlapScan scan = 
exprIdToScan.get(underlying.getExprId());
+                    if (scan != null) {
+                        exprIdToScan.put(ne.getExprId(), scan);
+                        String colName = 
exprIdToColName.get(underlying.getExprId());
+                        if (colName != null) {
+                            exprIdToColName.put(ne.getExprId(), colName);
+                        }
                     }
-                    StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
-                    if (delta != null) {
-                        sr.getOriginalColumn().ifPresent(col -> 
delta.addFilterStats(col.getName()));
+                } else if (underlying == null && ne instanceof Alias) {
+                    // Complex alias: Alias(Cast(col), name) — created by
+                    // PushDownExpressionsInHashCondition for type-mismatched 
join keys.
+                    // If all input slots trace back to one scan, propagate to 
the alias ExprId.
+                    Set<Slot> inputSlots = ((Alias) 
ne).child().getInputSlots();
+                    if (inputSlots.size() == 1) {
+                        Slot inputSlot = inputSlots.iterator().next();
+                        PhysicalOlapScan scan = 
exprIdToScan.get(inputSlot.getExprId());
+                        if (scan != null) {
+                            exprIdToScan.put(ne.getExprId(), scan);
+                            String colName = 
exprIdToColName.get(inputSlot.getExprId());
+                            if (colName != null) {
+                                exprIdToColName.put(ne.getExprId(), colName);
+                            }
+                        }
                     }
-                });
+                }
             }
         }
     }
 
+    private static void recordInputSlotsAsQueryHit(Expression expr,
+            Map<ExprId, PhysicalOlapScan> exprIdToScan,
+            Map<ExprId, String> exprIdToColName,
+            Map<String, StatsDelta> deltas) {
+        for (Slot slot : expr.getInputSlots()) {
+            if (!(slot instanceof SlotReference)) {
+                continue;
+            }
+            SlotReference sr = (SlotReference) slot;
+            PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
+            if (sourceScan == null) {
+                continue;
+            }
+            StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
+            if (delta != null) {
+                String colName = sr.getOriginalColumn().map(col -> 
col.getName())
+                        .orElseGet(() -> exprIdToColName.get(sr.getExprId()));
+                if (colName != null) {
+                    delta.addQueryStats(colName);
+                }
+            }
+        }
+    }
+
+    private static void recordInputSlotsAsFilterHit(Expression expr,
+            Map<ExprId, PhysicalOlapScan> exprIdToScan,
+            Map<ExprId, String> exprIdToColName,
+            Map<String, StatsDelta> deltas) {
+        for (Slot slot : expr.getInputSlots()) {
+            if (!(slot instanceof SlotReference)) {
+                continue;
+            }
+            SlotReference sr = (SlotReference) slot;
+            PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
+            if (sourceScan == null) {
+                continue;
+            }
+            StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
+            if (delta != null) {
+                String colName = sr.getOriginalColumn().map(col -> 
col.getName())
+                        .orElseGet(() -> exprIdToColName.get(sr.getExprId()));
+                if (colName != null) {
+                    delta.addFilterStats(colName);
+                }
+            }
+        }
+    }
+
+    /** Registers a slot's column name into exprIdToColName for later fallback 
lookup. */
+    private static void registerColName(Map<ExprId, String> exprIdToColName,
+            ExprId exprId, SlotReference slot) {
+        String name = slot.getOriginalColumn().map(col -> 
col.getName()).orElse(slot.getName());
+        if (name != null) {
+            exprIdToColName.put(exprId, name);
+        }
+    }
+
+    /** Unwraps Alias chains to reach the underlying SlotReference; null for 
computed expressions. */
+    static SlotReference unwrapAlias(Expression expr) {
+        if (expr instanceof SlotReference) {
+            return (SlotReference) expr;
+        }
+        if (expr instanceof Alias) {
+            return unwrapAlias(((Alias) expr).child());
+        }
+        return null;
+    }
+
     private static StatsDelta getOrCreateDelta(Map<String, StatsDelta> deltas,
             PhysicalOlapScan scan) {
         OlapTable t = scan.getTable();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsUtil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsUtil.java
index a8cf306f271..af261430a5a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsUtil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsUtil.java
@@ -148,7 +148,10 @@ public class QueryStatsUtil {
         request.setType(TQueryStatsType.TABLET);
         request.setReplicaId(replicaId);
         for (TQueryStatsResult other : getStats(request)) {
-            queryHits += other.getTabletStats().get(replicaId);
+            Long remoteCount = other.getTabletStats().get(replicaId);
+            if (remoteCount != null) {
+                queryHits += remoteCount;
+            }
         }
         return queryHits;
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/statistics/query/QueryStatsRecorderTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/statistics/query/QueryStatsRecorderTest.java
index a06a6eba4ad..7d747f55f57 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/statistics/query/QueryStatsRecorderTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/statistics/query/QueryStatsRecorderTest.java
@@ -26,13 +26,20 @@ import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.glue.LogicalPlanAdapter;
 import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.OrderExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.WindowExpression;
 import org.apache.doris.nereids.trees.plans.commands.Command;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterialize;
 import 
org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterializeOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
+import 
org.apache.doris.nereids.trees.plans.physical.PhysicalStorageLayerAggregate;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.qe.QueryState;
 
@@ -259,7 +266,63 @@ public class QueryStatsRecorderTest {
     }
 
     /**
-     * Plan: Filter(k1=1) → Scan[k1(id1)], root output = [k1].
+     * TopN lazy materialization: operative slot (sort_col/id2) is in the 
lazy-scan output;
+     * lazy slot (cold_col/id3) is only in PhysicalLazyMaterialize's output.
+     * Expected: cold_col.queryHit=true via the PhysicalLazyMaterialize walk 
branch.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testLazyMaterializeQueryHitRecorded() {
+        ExprId idSort   = new ExprId(1); // sort_col — operative, in lazy-scan 
output
+        ExprId idRowId  = new ExprId(2); // row-id slot
+        ExprId idCold   = new ExprId(3); // cold_col — lazy, NOT in lazy-scan 
output
+
+        SlotReference sortSlot  = mockSlot(idSort,  "sort_col");
+        SlotReference rowIdSlot = mockSlot(idRowId, "__row_id");
+        SlotReference coldSlot  = mockSlot(idCold,  "cold_col");
+
+        // Inner scan
+        PhysicalOlapScan inner = mockScan(1L, 1L, 1L, 1L,
+                ImmutableList.of(sortSlot, rowIdSlot));
+
+        // PhysicalLazyMaterializeOlapScan: output = [sort_col, row_id]
+        PhysicalLazyMaterializeOlapScan lazyScan =
+                Mockito.mock(PhysicalLazyMaterializeOlapScan.class);
+        Mockito.when(lazyScan.getScan()).thenReturn(inner);
+        Mockito.when(lazyScan.getOutput())
+                .thenReturn(ImmutableList.of(sortSlot, rowIdSlot));
+        Mockito.when(lazyScan.children()).thenReturn(ImmutableList.of());
+
+        // PhysicalLazyMaterialize: knows cold_col is lazy for this relation;
+        // uses rowIdSlot to link back to the relation.
+        org.apache.doris.nereids.trees.plans.algebra.Relation rel =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.algebra.Relation.class);
+
+        PhysicalLazyMaterialize<?> lazyMat =
+                Mockito.mock(PhysicalLazyMaterialize.class);
+        
Mockito.when(lazyMat.children()).thenReturn(ImmutableList.of(lazyScan));
+        Mockito.when(lazyMat.getRelations()).thenReturn(ImmutableList.of(rel));
+        
Mockito.when(lazyMat.getRowIds()).thenReturn(ImmutableList.of(rowIdSlot));
+        
Mockito.when(lazyMat.getLazySlots(rel)).thenReturn(ImmutableList.of(coldSlot));
+        // Root output: cold_col is selected
+        
Mockito.when(lazyMat.getOutput()).thenReturn(ImmutableList.of(coldSlot));
+
+        Map<String, StatsDelta> deltas =
+                QueryStatsRecorder.collectDeltas((PhysicalPlan) lazyMat);
+
+        Assertions.assertEquals(1, deltas.size());
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta, "delta must exist for the inner scan");
+        Assertions.assertNotNull(delta.getColumnStats().get("cold_col"),
+                "cold_col must be recorded via lazy-materialize branch");
+        Assertions.assertTrue(delta.getColumnStats().get("cold_col").queryHit,
+                "cold_col.queryHit must be true");
+        // sort_col was only an operative slot, not in root output — no 
queryHit
+        Assertions.assertNull(delta.getColumnStats().get("sort_col"),
+                "sort_col must not appear (not in root output)");
+    }
+
+    /**
      * k1 appears in both the WHERE predicate and the SELECT output.
      * Expected: k1.queryHit=true AND k1.filterHit=true simultaneously.
      */
@@ -405,6 +468,459 @@ public class QueryStatsRecorderTest {
         Assertions.assertTrue(deltas.isEmpty(), "Null-table scan must not 
create a delta");
     }
 
+    // ── Alias / GROUP BY / ORDER BY / Window 
─────────────────────────────────
+
+    /**
+ * Unit test for unwrapAlias: Alias(k1) → k1SlotReference.
+     */
+    @Test
+    public void testUnwrapAliasReturnsUnderlyingSlot() {
+        ExprId id1 = new ExprId(1);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+
+        org.apache.doris.nereids.trees.expressions.Alias alias =
+                
Mockito.mock(org.apache.doris.nereids.trees.expressions.Alias.class);
+        Mockito.when(alias.child()).thenReturn(k1Slot);
+
+        SlotReference result = QueryStatsRecorder.unwrapAlias(alias);
+        Assertions.assertEquals(k1Slot, result);
+    }
+
+    /**
+     * SELECT k1 AS x FROM t: PhysicalProject.getProjects() exposes Alias so
+     * unwrapAlias resolves to k1 and records k1.queryHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testAliasUnwrappedForQueryHit() {
+        ExprId id1 = new ExprId(1);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot));
+
+        org.apache.doris.nereids.trees.expressions.Alias alias =
+                
Mockito.mock(org.apache.doris.nereids.trees.expressions.Alias.class);
+        Mockito.when(alias.getExprId()).thenReturn(new ExprId(99));
+        Mockito.when(alias.child()).thenReturn(k1Slot);
+
+        org.apache.doris.nereids.trees.plans.physical.PhysicalProject<?> 
project =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalProject.class);
+        Mockito.when(project.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(project.getProjects()).thenReturn(ImmutableList.of(alias));
+        Mockito.when(project.getOutput()).thenReturn(ImmutableList.of());
+
+        Map<String, StatsDelta> deltas = QueryStatsRecorder.collectDeltas(
+                (org.apache.doris.nereids.trees.plans.physical.PhysicalPlan) 
project);
+
+        Assertions.assertEquals(1, deltas.size());
+        StatsDelta delta = deltas.values().iterator().next();
+        Assertions.assertNotNull(delta.getColumnStats().get("k1"), "k1 must be 
recorded via alias unwrap");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit);
+    }
+
+    /**
+     * SELECT k1 AS x FROM t ORDER BY k2: PhysicalProject is intermediate 
under PhysicalSort.
+     * walkPlan must propagate alias ExprId so parent's getOutput() resolves 
to k1.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testAliasUnwrappedForQueryHitIntermediateProject() {
+        ExprId id1 = new ExprId(1);
+        ExprId id2 = new ExprId(2);
+        ExprId aliasId = new ExprId(99);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        SlotReference k2Slot = mockSlot(id2, "k2");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot, k2Slot));
+
+        org.apache.doris.nereids.trees.expressions.Alias alias =
+                
Mockito.mock(org.apache.doris.nereids.trees.expressions.Alias.class);
+        Mockito.when(alias.getExprId()).thenReturn(aliasId);
+        Mockito.when(alias.child()).thenReturn(k1Slot);
+
+        // x_slot — what PhysicalProject.getOutput() returns (alias's ExprId, 
not k1's)
+        SlotReference xSlot = mockSlot(aliasId, "x");
+
+        org.apache.doris.nereids.trees.plans.physical.PhysicalProject<?> 
project =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalProject.class);
+        Mockito.when(project.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(project.getProjects()).thenReturn(ImmutableList.of(alias));
+        Mockito.when(project.getOutput()).thenReturn(ImmutableList.of(xSlot));
+
+        Expression sortExpr = Mockito.mock(Expression.class);
+        
Mockito.when(sortExpr.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot));
+        org.apache.doris.nereids.properties.OrderKey orderKey =
+                
Mockito.mock(org.apache.doris.nereids.properties.OrderKey.class);
+        Mockito.when(orderKey.getExpr()).thenReturn(sortExpr);
+
+        org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalSort<?> 
sort =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalSort.class);
+        Mockito.when(sort.children()).thenReturn(ImmutableList.of(project));
+        
Mockito.when(sort.getOrderKeys()).thenReturn(ImmutableList.of(orderKey));
+        Mockito.when(sort.getOutput()).thenReturn(ImmutableList.of(xSlot));
+
+        Map<String, StatsDelta> deltas = QueryStatsRecorder.collectDeltas(
+                (org.apache.doris.nereids.trees.plans.physical.PhysicalPlan) 
sort);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k2").queryHit, "k2: 
ORDER BY");
+        Assertions.assertNotNull(delta.getColumnStats().get("x"), "x slot 
resolves via alias propagation");
+        Assertions.assertTrue(delta.getColumnStats().get("x").queryHit, "x: 
SELECT output via alias");
+    }
+
+    /**
+     * SELECT k1, SUM(k2) FROM t GROUP BY k1: GROUP BY k1 → queryHit, SUM 
input k2 → queryHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testGroupByAndAggregateInputQueryHit() {
+        ExprId id1 = new ExprId(1);
+        ExprId id2 = new ExprId(2);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        SlotReference k2Slot = mockSlot(id2, "k2");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot, k2Slot));
+
+        Expression groupExpr = Mockito.mock(Expression.class);
+        
Mockito.when(groupExpr.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+
+        NamedExpression aggExpr = Mockito.mock(NamedExpression.class);
+        
Mockito.when(aggExpr.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot));
+
+        org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate<?> 
agg =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate.class);
+        Mockito.when(agg.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(agg.getGroupByExpressions()).thenReturn(ImmutableList.of(groupExpr));
+        
Mockito.when(agg.getOutputExpressions()).thenReturn(ImmutableList.of(aggExpr));
+        Mockito.when(agg.getOutput()).thenReturn(ImmutableList.of(k1Slot));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) agg);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
GROUP BY key");
+        Assertions.assertTrue(delta.getColumnStats().get("k2").queryHit, "k2: 
aggregate input");
+    }
+
+    /**
+     * SELECT k1 FROM t ORDER BY k2: ORDER BY k2 → queryHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testOrderByQueryHit() {
+        ExprId id1 = new ExprId(1);
+        ExprId id2 = new ExprId(2);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        SlotReference k2Slot = mockSlot(id2, "k2");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot, k2Slot));
+
+        Expression sortExpr = Mockito.mock(Expression.class);
+        
Mockito.when(sortExpr.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot));
+
+        org.apache.doris.nereids.properties.OrderKey orderKey =
+                
Mockito.mock(org.apache.doris.nereids.properties.OrderKey.class);
+        Mockito.when(orderKey.getExpr()).thenReturn(sortExpr);
+
+        org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalSort<?> 
sort =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalSort.class);
+        Mockito.when(sort.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(sort.getOrderKeys()).thenReturn(ImmutableList.of(orderKey));
+        Mockito.when(sort.getOutput()).thenReturn(ImmutableList.of(k1Slot));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) sort);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k2").queryHit, "k2: 
ORDER BY key");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
SELECT output");
+    }
+
+    /**
+     * Window PARTITION BY k0, ORDER BY k1: both → queryHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testWindowPartitionAndOrderQueryHit() {
+        ExprId id0 = new ExprId(1);
+        ExprId id1 = new ExprId(2);
+        SlotReference k0Slot = mockSlot(id0, "k0");
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k0Slot, k1Slot));
+
+        Expression partExpr = Mockito.mock(Expression.class);
+        
Mockito.when(partExpr.getInputSlots()).thenReturn(ImmutableSet.of(k0Slot));
+
+        Expression orderExprInner = Mockito.mock(Expression.class);
+        
Mockito.when(orderExprInner.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+
+        org.apache.doris.nereids.trees.expressions.OrderExpression orderExpr =
+                
Mockito.mock(org.apache.doris.nereids.trees.expressions.OrderExpression.class);
+        Mockito.when(orderExpr.child()).thenReturn(orderExprInner);
+
+        
org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup
 wfg =
+                Mockito.mock(
+                    
org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup.class);
+        
Mockito.when(wfg.getPartitionKeys()).thenReturn(ImmutableSet.of(partExpr));
+        
Mockito.when(wfg.getOrderKeys()).thenReturn(ImmutableList.of(orderExpr));
+
+        org.apache.doris.nereids.trees.plans.physical.PhysicalWindow<?> window 
=
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalWindow.class);
+        Mockito.when(window.children()).thenReturn(ImmutableList.of(scan));
+        Mockito.when(window.getWindowFrameGroup()).thenReturn(wfg);
+        Mockito.when(window.getOutput()).thenReturn(ImmutableList.of());
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) window);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k0").queryHit, "k0: 
PARTITION BY key");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
window ORDER BY key");
+    }
+
+    /**
+     * PhysicalBucketedHashAggregate implements Aggregate but does not extend 
PhysicalHashAggregate.
+     * The Aggregate interface check must cover it.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testBucketedAggregateGroupByQueryHit() {
+        ExprId id1 = new ExprId(1);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot));
+
+        Expression groupExpr = Mockito.mock(Expression.class);
+        
Mockito.when(groupExpr.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+
+        
org.apache.doris.nereids.trees.plans.physical.PhysicalBucketedHashAggregate<?> 
agg =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalBucketedHashAggregate.class);
+        Mockito.when(agg.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(agg.getGroupByExpressions()).thenReturn(ImmutableList.of(groupExpr));
+        
Mockito.when(agg.getOutputExpressions()).thenReturn(ImmutableList.of());
+        Mockito.when(agg.getOutput()).thenReturn(ImmutableList.of(k1Slot));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) agg);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta, "bucketed aggregate must be recorded 
via Aggregate interface");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
GROUP BY in bucketed agg");
+    }
+
+    /**
+     * JOIN ON t1.k1 = t2.k2: both hash-join and other-join conjuncts → 
filterHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testJoinConditionFilterHit() {
+        ExprId id1 = new ExprId(1);
+        ExprId id2 = new ExprId(2);
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        SlotReference k2Slot = mockSlot(id2, "k2");
+
+        PhysicalOlapScan left  = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot));
+        PhysicalOlapScan right = mockScan(2L, 2L, 2L, 2L, 
ImmutableList.of(k2Slot));
+
+        Expression hashConjunct = Mockito.mock(Expression.class);
+        
Mockito.when(hashConjunct.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot, 
k2Slot));
+
+        org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin<?, 
?> join =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin.class);
+        Mockito.when(join.children()).thenReturn(ImmutableList.of(left, 
right));
+        
Mockito.when(join.getHashJoinConjuncts()).thenReturn(ImmutableList.of(hashConjunct));
+        
Mockito.when(join.getOtherJoinConjuncts()).thenReturn(ImmutableList.of());
+        Mockito.when(join.getOutput()).thenReturn(ImmutableList.of(k1Slot));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) join);
+
+        Assertions.assertTrue(deltas.containsKey("1_1_1_1"));
+        Assertions.assertTrue(deltas.containsKey("2_2_2_2"));
+        
Assertions.assertTrue(deltas.get("1_1_1_1").getColumnStats().get("k1").filterHit,
+                "k1: join hash conjunct → filterHit");
+        
Assertions.assertTrue(deltas.get("2_2_2_2").getColumnStats().get("k2").filterHit,
+                "k2: join hash conjunct → filterHit");
+    }
+
+    /**
+     * JOIN ON a.k1 = b.k2 (tinyint vs smallint): Nereids inserts 
Alias(Cast(k1)) in a
+     * PhysicalProject via PushDownExpressionsInHashCondition. The cast-alias 
ExprId must be
+     * propagated into exprIdToScan so that recordInputSlotsAsFilterHit can 
record k1.filterHit.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testCastAliasInJoinPropagatesFilterHit() {
+        ExprId k1Id = new ExprId(1);
+        ExprId castAliasId = new ExprId(99);
+
+        SlotReference k1Slot = mockSlot(k1Id, "k1");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L, 
ImmutableList.of(k1Slot));
+
+        // Cast(k1) — not a SlotReference, so unwrapAlias returns null for the 
alias below
+        Expression castExpr = Mockito.mock(Expression.class);
+        
Mockito.when(castExpr.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+
+        // Alias(Cast(k1)) with a fresh ExprId — this is what 
PushDownExpressionsInHashCondition
+        // creates; the hash conjunct references castAliasId, not k1Id
+        org.apache.doris.nereids.trees.expressions.Alias castAlias =
+                
Mockito.mock(org.apache.doris.nereids.trees.expressions.Alias.class);
+        Mockito.when(castAlias.getExprId()).thenReturn(castAliasId);
+        Mockito.when(castAlias.child()).thenReturn(castExpr);
+
+        SlotReference castAliasSlot = mockSlot(castAliasId, "k1");
+        org.apache.doris.nereids.trees.plans.physical.PhysicalProject<?> 
project =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalProject.class);
+        Mockito.when(project.children()).thenReturn(ImmutableList.of(scan));
+        
Mockito.when(project.getProjects()).thenReturn(ImmutableList.of(castAlias));
+        
Mockito.when(project.getOutput()).thenReturn(ImmutableList.of(castAliasSlot));
+
+        // Hash conjunct uses the cast-alias slot, not the original k1Slot
+        Expression hashConjunct = Mockito.mock(Expression.class);
+        
Mockito.when(hashConjunct.getInputSlots()).thenReturn(ImmutableSet.of(castAliasSlot));
+
+        org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin<?, 
?> join =
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin.class);
+        Mockito.when(join.children()).thenReturn(ImmutableList.of(project));
+        
Mockito.when(join.getHashJoinConjuncts()).thenReturn(ImmutableList.of(hashConjunct));
+        
Mockito.when(join.getOtherJoinConjuncts()).thenReturn(ImmutableList.of());
+        Mockito.when(join.getOutput()).thenReturn(ImmutableList.of());
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) join);
+
+        Assertions.assertTrue(deltas.containsKey("1_1_1_1"), "scan delta must 
exist");
+        
Assertions.assertNotNull(deltas.get("1_1_1_1").getColumnStats().get("k1"),
+                "k1 must be recorded via cast-alias propagation");
+        
Assertions.assertTrue(deltas.get("1_1_1_1").getColumnStats().get("k1").filterHit,
+                "k1.filterHit must be set through Alias(Cast(k1)) chain");
+    }
+
+    /**
+     * SUM(k2) OVER (PARTITION BY k0 ORDER BY k1): k2 (value) → queryHit in 
addition to k0/k1.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testWindowFunctionValueColumnQueryHit() {
+        ExprId id0 = new ExprId(1);
+        ExprId id1 = new ExprId(2);
+        ExprId id2 = new ExprId(3);
+        SlotReference k0Slot = mockSlot(id0, "k0");
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        SlotReference k2Slot = mockSlot(id2, "k2");
+        PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
+                ImmutableList.of(k0Slot, k1Slot, k2Slot));
+
+        // Window function SUM(k2) — its input slots include k2
+        Expression sumFunc = Mockito.mock(Expression.class);
+        
Mockito.when(sumFunc.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot));
+
+        WindowExpression windowExpr = Mockito.mock(WindowExpression.class);
+        Mockito.when(windowExpr.getFunction()).thenReturn(sumFunc);
+        
Mockito.when(windowExpr.getInputSlots()).thenReturn(ImmutableSet.of(k0Slot, 
k1Slot, k2Slot));
+
+        NamedExpression windowAlias = Mockito.mock(NamedExpression.class);
+        Mockito.when(windowAlias.child(0)).thenReturn(windowExpr);
+
+        Expression partExpr = Mockito.mock(Expression.class);
+        
Mockito.when(partExpr.getInputSlots()).thenReturn(ImmutableSet.of(k0Slot));
+
+        OrderExpression orderExpr = Mockito.mock(OrderExpression.class);
+        Expression orderInner = Mockito.mock(Expression.class);
+        
Mockito.when(orderInner.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+        Mockito.when(orderExpr.child()).thenReturn(orderInner);
+
+        
org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup
 wfg =
+                Mockito.mock(
+                    
org.apache.doris.nereids.rules.implementation.LogicalWindowToPhysicalWindow.WindowFrameGroup.class);
+        
Mockito.when(wfg.getPartitionKeys()).thenReturn(ImmutableSet.of(partExpr));
+        
Mockito.when(wfg.getOrderKeys()).thenReturn(ImmutableList.of(orderExpr));
+        
Mockito.when(wfg.getGroups()).thenReturn(ImmutableList.of(windowAlias));
+
+        org.apache.doris.nereids.trees.plans.physical.PhysicalWindow<?> window 
=
+                
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalWindow.class);
+        Mockito.when(window.children()).thenReturn(ImmutableList.of(scan));
+        Mockito.when(window.getWindowFrameGroup()).thenReturn(wfg);
+        Mockito.when(window.getOutput()).thenReturn(ImmutableList.of());
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) window);
+
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k0").queryHit, "k0: 
PARTITION BY");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
window ORDER BY");
+        Assertions.assertTrue(delta.getColumnStats().get("k2").queryHit, "k2: 
SUM value column");
+    }
+
+    // ── helpers 
──────────────────────────────────────────────────────────────
+
+    @Test
+    public void testPhysicalStorageLayerAggregateRegistersQueryHit() {
+        Config.enable_query_hit_stats = true;
+        ExprId id0 = new ExprId(0);
+        SlotReference k0Slot = mockSlot(id0, "k0");
+        PhysicalOlapScan scan = mockScan(1, 1, 1, 1, ImmutableList.of(k0Slot));
+
+        PhysicalStorageLayerAggregate sla = 
Mockito.mock(PhysicalStorageLayerAggregate.class);
+        Mockito.when(sla.getRelation()).thenReturn(scan);
+        Mockito.when(sla.children()).thenReturn(ImmutableList.of());
+        Mockito.when(sla.getOutput()).thenReturn(ImmutableList.of(k0Slot));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) sla);
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta, "StorageLayerAggregate scan must 
produce a delta");
+        Assertions.assertTrue(delta.getColumnStats().get("k0").queryHit, "k0: 
COUNT(*)/agg pushdown");
+    }
+
+    @Test
+    public void testPhysicalRepeatRegistersGroupingExpressionsAsQueryHit() {
+        Config.enable_query_hit_stats = true;
+        ExprId id0 = new ExprId(0);
+        ExprId id1 = new ExprId(1);
+        SlotReference k0Slot = mockSlot(id0, "k0");
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        PhysicalOlapScan scan = mockScan(1, 1, 1, 1, ImmutableList.of(k0Slot, 
k1Slot));
+
+        Expression groupExpr = Mockito.mock(Expression.class);
+        
Mockito.when(groupExpr.getInputSlots()).thenReturn(ImmutableSet.of(k0Slot));
+
+        PhysicalRepeat<?> repeat = Mockito.mock(PhysicalRepeat.class);
+        Mockito.when(repeat.children()).thenReturn(ImmutableList.of(scan));
+        Mockito.when(repeat.getOutput()).thenReturn(ImmutableList.of());
+        Mockito.when(repeat.getGroupingSets())
+                .thenReturn(ImmutableList.of(ImmutableList.of(groupExpr)));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) repeat);
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k0").queryHit, "k0: 
ROLLUP grouping key");
+        Assertions.assertNull(delta.getColumnStats().get("k1"), "k1 not in 
grouping set — no hit");
+    }
+
+    @Test
+    public void 
testPhysicalPartitionTopNRegistersPartitionAndOrderKeysAsQueryHit() {
+        Config.enable_query_hit_stats = true;
+        ExprId id0 = new ExprId(0);
+        ExprId id1 = new ExprId(1);
+        SlotReference k0Slot = mockSlot(id0, "k0");
+        SlotReference k1Slot = mockSlot(id1, "k1");
+        PhysicalOlapScan scan = mockScan(1, 1, 1, 1, ImmutableList.of(k0Slot, 
k1Slot));
+
+        Expression partExpr = Mockito.mock(Expression.class);
+        
Mockito.when(partExpr.getInputSlots()).thenReturn(ImmutableSet.of(k0Slot));
+
+        Expression orderInner = Mockito.mock(Expression.class);
+        
Mockito.when(orderInner.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot));
+        org.apache.doris.nereids.properties.OrderKey orderKey =
+                
Mockito.mock(org.apache.doris.nereids.properties.OrderKey.class);
+        Mockito.when(orderKey.getExpr()).thenReturn(orderInner);
+
+        PhysicalPartitionTopN<?> ptn = 
Mockito.mock(PhysicalPartitionTopN.class);
+        Mockito.when(ptn.children()).thenReturn(ImmutableList.of(scan));
+        Mockito.when(ptn.getOutput()).thenReturn(ImmutableList.of());
+        
Mockito.when(ptn.getPartitionKeys()).thenReturn(ImmutableList.of(partExpr));
+        
Mockito.when(ptn.getOrderKeys()).thenReturn(ImmutableList.of(orderKey));
+
+        Map<String, StatsDelta> deltas = 
QueryStatsRecorder.collectDeltas((PhysicalPlan) ptn);
+        StatsDelta delta = deltas.get("1_1_1_1");
+        Assertions.assertNotNull(delta);
+        Assertions.assertTrue(delta.getColumnStats().get("k0").queryHit, "k0: 
PARTITION BY");
+        Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit, "k1: 
ORDER BY in PartitionTopN");
+    }
+
     // ── helpers 
──────────────────────────────────────────────────────────────
 
     private SlotReference mockSlot(ExprId exprId, String columnName) {
diff --git a/regression-test/suites/query_p0/stats/query_stats_test.groovy 
b/regression-test/suites/query_p0/stats/query_stats_test.groovy
index aa9b7e5a0b6..9a445e88e27 100644
--- a/regression-test/suites/query_p0/stats/query_stats_test.groovy
+++ b/regression-test/suites/query_p0/stats/query_stats_test.groovy
@@ -135,7 +135,7 @@ suite("query_stats_test") {
         assertEquals(0, row[2] as int)
     }
 
-    // Alias gap: k1.queryHit = 0 until Part 2 walks Project nodes.
+    // Alias: k1.queryHit >= 1 now that alias is unwrapped; k2.filterHit >= 1 
from WHERE.
     sql "clean all query stats"
     sql "select k1 as x from ${tbName} where k2 = 1"
     def aliasResult = sql "show query stats from ${tbName}"
@@ -143,10 +143,56 @@ suite("query_stats_test") {
     def arK2 = aliasResult.find { it[0] == "k2" }
     assertNotNull(arK1)
     assertNotNull(arK2)
-    assertEquals(0, arK1[1] as int)
+    assertTrue((arK1[1] as int) >= 1)
     assertTrue((arK2[2] as int) >= 1)
 
+    // GROUP BY: k1 queryHit from GROUP BY key, k2 queryHit from SUM(k2) 
aggregate input.
+    sql "clean all query stats"
+    sql "select k1, sum(k2) from ${tbName} group by k1"
+    def gbResult = sql "show query stats from ${tbName}"
+    def gbK1 = gbResult.find { it[0] == "k1" }
+    def gbK2 = gbResult.find { it[0] == "k2" }
+    assertNotNull(gbK1)
+    assertNotNull(gbK2)
+    assertTrue((gbK1[1] as int) >= 1)
+    assertTrue((gbK2[1] as int) >= 1)
+
+    // ORDER BY: k3 queryHit from SELECT, k4 queryHit from ORDER BY.
+    sql "clean all query stats"
+    sql "select k3 from ${tbName} order by k4"
+    def obResult = sql "show query stats from ${tbName}"
+    def obK3 = obResult.find { it[0] == "k3" }
+    def obK4 = obResult.find { it[0] == "k4" }
+    assertNotNull(obK3)
+    assertNotNull(obK4)
+    assertTrue((obK3[1] as int) >= 1)
+    assertTrue((obK4[1] as int) >= 1)
+
+    // JOIN: k1 filterHit from ON condition (same-type columns avoid 
cast-alias edge cases).
+    sql "clean all query stats"
+    sql """select a.k3 from ${tbName} a join ${tbName} b on a.k1 = b.k1"""
+    def joinOnResult = sql "show query stats from ${tbName}"
+    def jK1 = joinOnResult.find { it[0] == "k1" }
+    assertNotNull(jK1)
+    assertTrue((jK1[2] as int) >= 1)
+
+    // Window value column: k2 queryHit from SUM(k2), k0 from PARTITION BY, k1 
from ORDER BY.
+    sql "clean all query stats"
+    sql "select sum(k2) over (partition by k0 order by k1) from ${tbName}"
+    def winResult = sql "show query stats from ${tbName}"
+    def wK0 = winResult.find { it[0] == "k0" }
+    def wK1 = winResult.find { it[0] == "k1" }
+    def wK2 = winResult.find { it[0] == "k2" }
+    assertNotNull(wK0)
+    assertNotNull(wK1)
+    assertNotNull(wK2)
+    assertTrue((wK0[1] as int) >= 1)
+    assertTrue((wK1[1] as int) >= 1)
+    assertTrue((wK2[1] as int) >= 1)
+
     // Self-join: StatsDelta dedup keeps table count = 1 per FE.
+    // Use >= 1 rather than == 1: show query stats all aggregates across all 
FEs
+    // in a multi-FE cluster, so the total may exceed 1 even for a single 
query.
     sql "clean all query stats"
     sql "select a.k1 from ${tbName} a, ${tbName} b where a.k1 = b.k1"
     def joinResult = sql "show query stats from ${tbName} all"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to