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]