github-actions[bot] commented on code in PR #66898:
URL: https://github.com/apache/doris/pull/66898#discussion_r4043388612


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java:
##########
@@ -620,6 +637,22 @@ public Statistics computeOlapScan(OlapScan olapScan) {
         return computeVirtualColumnStats(olapScan, builder.build());
     }
 
+    /**
+     * Returns the slots whose column stats should be fetched for the query.
+     *
+     * <p>Only operative slots' column stats are needed by the query, column 
stats of other slots are
+     * useless and fetching them would pollute the column stats cache and 
waste time on wide tables.
+     * If operative slots are not derived yet (e.g. stats derivation during 
RBO) or full stats
+     * fidelity is required (forbidUnknownColStats), fall back to all output 
slots.
+     */
+    private List<Slot> getStatsNeededSlots(OlapScan olapScan) {
+        if (forbidUnknownColStats) {

Review Comment:
   [P1] Preserve a derived-empty operative set. The existing test establishes 
this real plan shape:
   
   ```text
   PhysicalResultSink(*)
     OlapScan(wide_table, operative=[])
   ```
   
   `OperativeColumnDerive` legitimately writes the empty list, but this 
fallback treats the same value as 'derivation not run' and expands it to every 
scan output. The scan therefore calls the loading cache API for every visible 
column, defeating the optimization. Please represent derivation state 
separately from the slot collection (or otherwise distinguish a derived empty 
set) and cover derivation plus scan-stat loading end to end.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/cache/StatisticsCache.java:
##########
@@ -136,6 +155,28 @@ private ColumnStatistic doGetColumnStatistics(
         return ColumnStatistic.UNKNOWN;
     }
 
+    /**
+     * Returns the column statistic only when it is already present in the 
cache, without
+     * triggering a cache load. Used for columns whose stats are not needed by 
the query,
+     * so that useless columns do not pollute the cache nor issue queries to 
the stats table.
+     */
+    private ColumnStatistic doGetColumnStatisticsIfPresent(
+            long catalogId, long dbId, long tblId, long idxId, String colName, 
ConnectContext ctx) {
+        StatisticsCacheKey k = new StatisticsCacheKey(catalogId, dbId, tblId, 
idxId, colName);
+        CompletableFuture<Optional<ColumnStatistic>> f = 
columnStatisticsCache.getIfPresent(k);

Review Comment:
   [P2] Use a quiet cache probe here. In Caffeine 3.2.3, 
[`LocalAsyncCache.getIfPresent`](https://github.com/ben-manes/caffeine/blob/v3.2.3/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java#L64-L67)
 delegates to the recording lookup, and 
[`BoundedLocalCache.getIfPresent`](https://github.com/ben-manes/caffeine/blob/v3.2.3/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java#L2044-L2071)
 calls `afterRead`, which invokes `refreshIfNeeded`. A refresh-eligible 
non-operative entry can therefore call the loader and query the statistics 
table, while every hit also updates access policy. That contradicts this 
method's no-load/no-pollution contract. Please use the synchronous view's quiet 
policy probe 
(`columnStatisticsCache.synchronous().policy().getIfPresentQuietly(k)`) and add 
a real-cache refresh test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java:
##########
@@ -415,6 +415,13 @@ public class Rewriter extends AbstractBatchJobExecutor {
                                     topDown(new LimitSortToTopN()),
                                     topDown(new SplitLimit()),
                                     custom(RuleType.SET_PREAGG_STATUS, 
SetPreAggStatus::new),
+                                    // Derive operative columns on the plan 
recorded for materialized view
+                                    // pre rewrite: the pre rewrite runs a 
cost-based optimization on this
+                                    // recorded plan to choose the best 
materialized view, and without
+                                    // operative slots computeOlapScan would 
fall back to fetching column
+                                    // stats of all table columns. The 
derivation is repeated before "init
+                                    // join" and at the end of rewrite, where 
the operative slots of the
+                                    // plans finally stored into the memo are 
recomputed.
                                     custom(RuleType.OPERATIVE_COLUMN_DERIVE, 
OperativeColumnDerive::new),

Review Comment:
   [P2] The MV-stage pass is still too late for this reachable CTE shape:
   
   ```text
   CTEAnchor
     CTEProducer -> Project(a) -> Scan(wide[a,b,...], operative=[])
     CTEConsumer
   ```
   
   `RewriteCteChildren.visitLogicalCTEAnchor` first runs `StatsDerive` on 
`cteAnchor.child(0)` (lines 93-94) and only later recursively applies this job 
list to the producer. The normal whole-tree CTE path has the same prepass, so 
it initiates a load for every visible column before either new derivation can 
help. Please derive producer operative slots before that prerequisite stats 
pass (or defer it), with ordinary-CTE and MV-pre-rewrite coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java:
##########
@@ -678,6 +685,13 @@ public class Rewriter extends AbstractBatchJobExecutor {
                         topDown(new PushDownAggThroughJoinOnPkFk()),
                         topDown(new PullUpJoinFromUnionAll())
                 ),
+                // RBO rules that depend on statistics (e.g. InitJoinOrder, 
SkewJoin, Eager
+                // aggregation, DecomposeRepeatWithPreAggregation, 
DistinctAggStrategySelector)
+                // must be placed AFTER OperativeColumnDerive: 
StatsCalculator.computeOlapScan
+                // only fetches column stats of operative slots, so rules 
running before the
+                // derivation would fetch stats of all table columns, 
polluting the column stats
+                // cache and wasting time on wide tables.
+                custom(RuleType.OPERATIVE_COLUMN_DERIVE, 
OperativeColumnDerive::new),

Review Comment:
   [P2] Run this before `InferSetOperatorDistinct`. The reachable order is:
   
   ```text
   Intersect DISTINCT
     Project(a)
       OlapScan(wide[a,b,...], operative=[])
   ```
   
   That rule is scheduled at line 614 and calls `StatsDerive` when stats are 
absent, so the new fallback initiates loads for every visible column before 
this line runs. This is distinct from the existing thread about removing other 
passes: the defect is an earlier stats consumer bypassing all three. Please 
place operative derivation before the first rewrite-time stats consumer and add 
a planner-level set-operation case.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java:
##########
@@ -826,6 +841,11 @@ public class Rewriter extends AbstractBatchJobExecutor {
                         )
                 ),
                 topDown(new CollectCteConsumerOutput()),
+                // Re-derive operative columns at the end of rewrite: rules 
after the early
+                // OperativeColumnDerive (before "init join") may rebuild 
scans or add virtual
+                // columns (e.g. stream scan normalization, variant virtual 
column push down),

Review Comment:
   [P2] Re-derive immediately after stream-scan normalization. The reduced 
post-normalization tree is:
   
   ```text
   Join(stream.k = t.k)
     Project -> replacement OlapScan(wide, operative=[])
     Scan(t)
   ```
   
   Normalization creates fresh scans with empty operative lists, and `SkewJoin` 
runs before this final pass and calls `StatsDerive` on unstatted children. It 
can therefore initiate all-column loads before line 849 repairs the slots. This 
is distinct from the existing reply that normalization requires a final pass: 
the intervening stats consumer runs before that repair. Please move/add the 
pass before the first post-normalization stats consumer and cover a wide 
stream-scan join.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java:
##########
@@ -609,7 +619,14 @@ public Statistics computeOlapScan(OlapScan olapScan) {
         } else {
             // get table level stats
             for (SlotReference slot : visibleOutputSlots) {
-                ColumnStatistic cache = 
olapTableStats.getColumnStatistics(slot.getName(), connectContext);
+                ColumnStatistic cache;
+                if (!statsNeededSlots.contains(slot)) {

Review Comment:
   [P1] Keep carried-output width deterministic. A reduced reachable tree is:
   
   ```text
   ResultSink(r.payload)
     Project(r.payload)
       HashJoin(l.k = r.k)
         Scan(l.k)
         Scan(r.k, r.payload)
   ```
   
   Only `r.k` is operative, but `r.payload` remains in the join output. Its 
`avgSizeByte` is consumed by `computeTupleSize`, exchange costs, and 
`checkBroadcastJoinStats`' hard memory gate. Here a cold cache gives UNKNOWN 
(`avgSizeByte=1`) while an unrelated warm cache gives the analyzed width, so 
the same wide build can alternate between broadcast-eligible and ineligible. 
Please use a cache-independent type/schema width for unloaded outputs or 
include every slot used by byte-size gates, and test cold versus prewarmed 
behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to