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 1b5b76a1bc5984..78ce44dc3aff76 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.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. * - *

Scope (Part 1): - *

- * GROUP BY, ORDER BY, window, JOIN, and aliased/projected columns are deferred to Part 2. + *

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 static void record(PhysicalPlan plan, StatementContext stmtContext) { */ static Map collectDeltas(PhysicalPlan plan) { Map exprIdToScan = new HashMap<>(); + Map exprIdToColName = new HashMap<>(); Map 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 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 @@ static boolean shouldRecord(StatementContext ctx) { /** * 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 exprIdToScan, + Map exprIdToColName, Map 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 @@ private static void walkPlan(Plan plan, 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 rels = lazy.getRelations(); + List 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 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 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 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 exprIdToScan, + Map exprIdToColName, + Map 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 exprIdToScan, + Map exprIdToColName, + Map 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 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 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 a8cf306f271be7..af261430a5af6a 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 static long getMergedReplicaStats(long replicaId) { 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 a06a6eba4ad41f..7d747f55f572a4 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.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 void testDeferMaterializeScanUsesInnerScan() { } /** - * 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 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 void testNullTableInScanDoesNotCrash() { 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 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 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 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 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 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 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 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 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 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 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 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 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 aa9b7e5a0b6fcb..bc6c16f7fc202e 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,14 +143,58 @@ 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. 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" - assertTrue((joinResult[0][1] as int) >= 1) + assertEquals(1, joinResult[0][1] as int) sql "admin set all frontends config (\"enable_query_hit_stats\"=\"false\");" sql "set enable_nereids_planner = ${origNereids}"