englefly commented on code in PR #67549:
URL: https://github.com/apache/doris/pull/67549#discussion_r4244253264


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1138,13 +1292,198 @@ public String getExplainString(ExplainOptions 
explainOptions) {
             plan += "\n\n\n group expression count exceeds 
memo_max_group_expression_size("
                     + 
ConnectContext.get().getSessionVariable().memoMaxGroupExpressionSize + ")\n";
         }
-        if (statementContext != null) {
-            if (!statementContext.getHints().isEmpty()) {
-                String hint = 
getHintExplainString(statementContext.getHints());
-                return plan + hint;
+        String hint = "";
+        if (statementContext != null && 
!statementContext.getHints().isEmpty()) {
+            hint = getHintExplainString(statementContext.getHints());
+        }
+        // the annotation block is meaningful only once a physical plan exists 
(fragment-form
+        // explain); parsed/analyzed/rewritten tree output must not carry the 
misleading
+        // "(no hbo fingerprint attached ...)" fallback
+        if (explainLevel != ExplainLevel.PARSED_PLAN
+                && explainLevel != ExplainLevel.ANALYZED_PLAN
+                && explainLevel != ExplainLevel.REWRITTEN_PLAN
+                && ConnectContext.get() != null
+                && 
ConnectContext.get().getSessionVariable().isShowHboFingerprint()
+                && physicalPlan != null && cascadesContext != null) {
+            plan += appendHboFingerprintAnnotations();
+        }
+        return plan + hint;
+    }
+
+    /**
+     * Append per-node hbo fingerprint annotations (join / aggregation / 
filter) to the explain
+     * string. Controlled by session variable {@code show_hbo_fingerprint} 
(default off) so that
+     * the default explain output is unchanged. For a filter that sits above 
an olap scan
+     * (filter-on-scan) the annotation shows the fingerprint of the scan 
group, which is the key
+     * used by the hbo read side to override the filter row count — users copy 
the quoted
+     * {@code fingerprint=} / {@code struct=} (or {@code condFingerprint=} / 
{@code cond=} for a
+     * join expansion) straight into the {@code HBO SET STATISTICS} statement.
+     */
+    private String appendHboFingerprintAnnotations() {
+        StringBuilder sb = new StringBuilder("\n\nHBO fingerprint annotations 
(join/aggregation/filter):\n");
+        if (ConnectContext.get() != null && 
!ConnectContext.get().getSessionVariable().isEnableHboOptimization()) {
+            sb.append("  note: enable_hbo_optimization is off; injected hbo 
statistics will not take "
+                    + "effect until it is enabled\n");
+        }
+        List<AbstractPlan> nodes = new ArrayList<>();
+        collectPlanNodes(physicalPlan, nodes);
+        nodes.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
+        Map<String, String> guardSkips = Collections.emptyMap();
+        Map<String, String> applyStates = Collections.emptyMap();
+        Map<String, String> appliedModes = Collections.emptyMap();
+        if (ConnectContext.get() != null) {
+            HboPlanInfoProvider provider = 
Env.getCurrentEnv().getHboPlanStatisticsManager()
+                    .getHboPlanInfoProvider();
+            String queryId = DebugUtil.printId(ConnectContext.get().queryId());
+            guardSkips = provider.getPinnedGuardSkip(queryId);
+            applyStates = provider.getPinnedApplyState(queryId);
+            appliedModes = provider.getPinnedLiteralMode(queryId);
+        }
+        for (AbstractPlan node : nodes) {
+            String kind;
+            if (node instanceof AbstractPhysicalJoin) {
+                kind = "join";
+            } else if (node instanceof PhysicalHashAggregate || node 
instanceof PhysicalStorageLayerAggregate) {
+                kind = "aggregation";
+            } else if (node instanceof PhysicalFilter) {
+                // a filter root is injectable: its primary fingerprint 
carries the literals, and
+                // the constant agnostic shape fingerprint is printed next to 
it; filters directly
+                // above a scan additionally have a learned read-side key 
derived from the scan
+                AbstractPlan scan = findScanUnder((PhysicalFilter<?>) node);
+                kind = scan == null ? "filter" : "filter-on-scan(table=" + 
scanName(scan) + ")";
+            } else {
+                continue;
+            }
+            Object fingerprint = 
node.getMutableState(MutableState.KEY_HBO_FP).orElse(null);
+            Object struct = 
node.getMutableState(MutableState.KEY_HBO_STRUCT).orElse(null);
+            if (fingerprint == null || struct == null) {
+                Object skip = 
node.getMutableState(MutableState.KEY_HBO_STRUCT_SKIP).orElse(null);
+                if (skip != null) {
+                    // a node whose sub tree is too wide for hbo: it has no 
fingerprint by design, and
+                    // saying so is the difference between "hbo is off for 
this shape" and "hbo broke"
+                    sb.append("  [").append(node.getId()).append("] 
").append(kind)
+                            .append(" hbo fingerprint skipped: 
").append(skip).append('\n');
+                }
+                continue;
+            }
+            Object expansion = 
node.getMutableState(MutableState.KEY_HBO_EXPANSION).orElse(null);
+            Object condFingerprint = 
node.getMutableState(MutableState.KEY_HBO_COND_FP).orElse(null);
+            Object cond = 
node.getMutableState(MutableState.KEY_HBO_COND).orElse(null);
+            // the type of the row count entry is read from the entry of that 
line's own fingerprint
+            // (EXACT unless a guarded entry was injected), so the two lines 
of a filter root can not
+            // report each other's type
+            if (node instanceof PhysicalFilter) {
+                // a filter root can be keyed in both literal modes: the 
literal carrying form (only
+                // that constant matches) and the constant agnostic form 
(every constant of the
+                // predicate shape matches), so both are printed as separate 
injectable entries
+                appendHboEntryLine(sb, node.getId(), kind, "with_literal",
+                        hboEntryTypeOf(String.valueOf(fingerprint)), 
fingerprint, struct,
+                        guardSkips, applyStates, appliedModes, expansion, 
false);
+                Object noLiteralFingerprint = 
node.getMutableState(MutableState.KEY_HBO_FP_NO_LITERAL).orElse(null);
+                Object noLiteralStruct = 
node.getMutableState(MutableState.KEY_HBO_STRUCT_NO_LITERAL).orElse(null);
+                if (noLiteralFingerprint != null && noLiteralStruct != null
+                        && !noLiteralFingerprint.equals(fingerprint)) {
+                    appendHboEntryLine(sb, node.getId(), kind, "no_literal",
+                            
hboEntryTypeOf(String.valueOf(noLiteralFingerprint)), noLiteralFingerprint,
+                            noLiteralStruct, guardSkips, applyStates, 
appliedModes, expansion, false);
+                }
+            } else {
+                // a join / aggregation entry only has the constant agnostic 
form
+                appendHboEntryLine(sb, node.getId(), kind, "no_literal",
+                        hboEntryTypeOf(String.valueOf(fingerprint)), 
fingerprint, struct,
+                        guardSkips, applyStates, appliedModes, expansion, 
false);
             }
+            if (condFingerprint != null && cond != null) {
+                // the join condition key of this node: an expansion entry, 
kept in the same table and
+                // listed like any other entry of this node
+                appendHboEntryLine(sb, node.getId(), kind, "no_literal", 
"join_expansion", condFingerprint,
+                        cond, guardSkips, applyStates, appliedModes, 
expansion, true);
+            }
+        }
+        if (sb.toString().indexOf("fingerprint=") < 0) {
+            sb.append("  (no hbo fingerprint attached; check that the plan 
went through the "
+                    + "planner attach step)\n");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Append one injectable hbo entry of a node. The parameters are printed 
with the spelling
+     * {@code HBO SET STATISTICS} uses (quoted values), so the line can be 
copied as it is, and the
+     * line of the entry which was actually used (or skipped by its guard) is 
marked.
+     */
+    private void appendHboEntryLine(StringBuilder sb, int nodeId, String kind, 
String literalMode,
+            String type, Object fingerprint, Object struct, Map<String, 
String> guardSkips,
+            Map<String, String> applyStates, Map<String, String> appliedModes, 
Object expansion,
+            boolean expansionLine) {
+        String key = String.valueOf(fingerprint);
+        sb.append("  [").append(nodeId).append("] ").append(kind)
+                .append(" type=").append(type)
+                .append(" literal_mode=").append(literalMode)
+                .append(" fingerprint='").append(key).append("'")
+                .append(" struct='").append(struct).append("'");

Review Comment:
   认可,已修复(0cbdc52d362):EXPLAIN 的 struct= 现在按字符串字面量输出(单引号包裹,' 折半、反斜杠转义),与 SET 侧 
parseStringLiteral 的解析互为逆运算,含撇号的 canonical 可以直接复制粘贴。



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1138,13 +1292,198 @@ public String getExplainString(ExplainOptions 
explainOptions) {
             plan += "\n\n\n group expression count exceeds 
memo_max_group_expression_size("
                     + 
ConnectContext.get().getSessionVariable().memoMaxGroupExpressionSize + ")\n";
         }
-        if (statementContext != null) {
-            if (!statementContext.getHints().isEmpty()) {
-                String hint = 
getHintExplainString(statementContext.getHints());
-                return plan + hint;
+        String hint = "";
+        if (statementContext != null && 
!statementContext.getHints().isEmpty()) {
+            hint = getHintExplainString(statementContext.getHints());
+        }
+        // the annotation block is meaningful only once a physical plan exists 
(fragment-form
+        // explain); parsed/analyzed/rewritten tree output must not carry the 
misleading
+        // "(no hbo fingerprint attached ...)" fallback
+        if (explainLevel != ExplainLevel.PARSED_PLAN
+                && explainLevel != ExplainLevel.ANALYZED_PLAN
+                && explainLevel != ExplainLevel.REWRITTEN_PLAN
+                && ConnectContext.get() != null
+                && 
ConnectContext.get().getSessionVariable().isShowHboFingerprint()
+                && physicalPlan != null && cascadesContext != null) {
+            plan += appendHboFingerprintAnnotations();
+        }
+        return plan + hint;
+    }
+
+    /**
+     * Append per-node hbo fingerprint annotations (join / aggregation / 
filter) to the explain
+     * string. Controlled by session variable {@code show_hbo_fingerprint} 
(default off) so that
+     * the default explain output is unchanged. For a filter that sits above 
an olap scan
+     * (filter-on-scan) the annotation shows the fingerprint of the scan 
group, which is the key
+     * used by the hbo read side to override the filter row count — users copy 
the quoted
+     * {@code fingerprint=} / {@code struct=} (or {@code condFingerprint=} / 
{@code cond=} for a
+     * join expansion) straight into the {@code HBO SET STATISTICS} statement.
+     */
+    private String appendHboFingerprintAnnotations() {
+        StringBuilder sb = new StringBuilder("\n\nHBO fingerprint annotations 
(join/aggregation/filter):\n");
+        if (ConnectContext.get() != null && 
!ConnectContext.get().getSessionVariable().isEnableHboOptimization()) {
+            sb.append("  note: enable_hbo_optimization is off; injected hbo 
statistics will not take "
+                    + "effect until it is enabled\n");
+        }
+        List<AbstractPlan> nodes = new ArrayList<>();
+        collectPlanNodes(physicalPlan, nodes);
+        nodes.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
+        Map<String, String> guardSkips = Collections.emptyMap();
+        Map<String, String> applyStates = Collections.emptyMap();
+        Map<String, String> appliedModes = Collections.emptyMap();
+        if (ConnectContext.get() != null) {
+            HboPlanInfoProvider provider = 
Env.getCurrentEnv().getHboPlanStatisticsManager()
+                    .getHboPlanInfoProvider();
+            String queryId = DebugUtil.printId(ConnectContext.get().queryId());
+            guardSkips = provider.getPinnedGuardSkip(queryId);
+            applyStates = provider.getPinnedApplyState(queryId);
+            appliedModes = provider.getPinnedLiteralMode(queryId);
+        }
+        for (AbstractPlan node : nodes) {
+            String kind;
+            if (node instanceof AbstractPhysicalJoin) {
+                kind = "join";
+            } else if (node instanceof PhysicalHashAggregate || node 
instanceof PhysicalStorageLayerAggregate) {
+                kind = "aggregation";
+            } else if (node instanceof PhysicalFilter) {
+                // a filter root is injectable: its primary fingerprint 
carries the literals, and
+                // the constant agnostic shape fingerprint is printed next to 
it; filters directly
+                // above a scan additionally have a learned read-side key 
derived from the scan
+                AbstractPlan scan = findScanUnder((PhysicalFilter<?>) node);
+                kind = scan == null ? "filter" : "filter-on-scan(table=" + 
scanName(scan) + ")";
+            } else {
+                continue;
+            }
+            Object fingerprint = 
node.getMutableState(MutableState.KEY_HBO_FP).orElse(null);
+            Object struct = 
node.getMutableState(MutableState.KEY_HBO_STRUCT).orElse(null);
+            if (fingerprint == null || struct == null) {
+                Object skip = 
node.getMutableState(MutableState.KEY_HBO_STRUCT_SKIP).orElse(null);
+                if (skip != null) {
+                    // a node whose sub tree is too wide for hbo: it has no 
fingerprint by design, and
+                    // saying so is the difference between "hbo is off for 
this shape" and "hbo broke"
+                    sb.append("  [").append(node.getId()).append("] 
").append(kind)
+                            .append(" hbo fingerprint skipped: 
").append(skip).append('\n');
+                }
+                continue;

Review Comment:
   认可,已修复(0cbdc52d362):attach 阶段先读取 cond fingerprint/canonical,即使本节点的行数 key 
缺失(例如 unsupported sort 之上的 hash join),仍会打印它的 join condition 行;不再用 continue 把可用的 
expansion key 一起吞掉。



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/stats/HboPlanStatisticsManager.java:
##########
@@ -38,4 +187,415 @@ public HboPlanStatisticsProvider 
getHboPlanStatisticsProvider() {
     public HboPlanInfoProvider getHboPlanInfoProvider() {
         return hboPlanInfoProvider;
     }
+
+    /**
+     * Inject (or overwrite) a pinned statistics entry applied unconditionally.
+     * @param fingerprint hbo fingerprint (simplified group struct info sha256)
+     * @param rows output row count that overrides the optimizer estimation
+     * @param structCanonical optional human-readable simplified struct info 
canonical string
+     */
+    public void putPinnedPlanStatistics(String fingerprint, long rows, String 
structCanonical) {
+        putPinnedPlanStatistics(fingerprint, rows, PinnedType.EXACT, 
structCanonical, LiteralMode.NO_LITERAL);
+    }
+
+    /**
+     * Inject (or overwrite) a pinned statistics entry.
+     * @param type how the injected row count is applied (unconditional vs 
filter-small guard)
+     * @param structCanonical optional human-readable simplified struct info 
canonical string
+     */
+    public void putPinnedPlanStatistics(String fingerprint, long rows, 
PinnedType type, String structCanonical,
+            LiteralMode literalMode) {
+        putPinnedStatistics(fingerprint, rows, type, structCanonical, 0, 
literalMode);
+    }
+
+    /**
+     * Inject (or overwrite) a pinned statistics entry.
+     *
+     * @param expansion fan-out factor, only for {@link 
PinnedType#JOIN_EXPANSION} entries
+     */
+    private void putPinnedStatistics(String fingerprint, long rows, PinnedType 
type, String structCanonical,
+            double expansion, LiteralMode literalMode) {
+        // LRU bounded by Config.hbo_pinned_stats_cache_num; pinned entries 
are otherwise never
+        // expired automatically and are only removed by HBO DELETE STATISTICS 
or eviction
+        long createTimeMs = System.currentTimeMillis();
+        synchronized (pinnedLoadLock) {
+            pinnedPlanStatistics.put(fingerprint,
+                    new PinnedHboStatistics(fingerprint, rows, type, 
structCanonical, expansion, literalMode,
+                            createTimeMs));
+            if (type != PinnedType.JOIN_EXPANSION) {
+                // only row count entries are looked up by relation key: a 
join expansion entry is
+                // keyed by the join conditions, is rejected by 
getPinnedPlanStatistics, and its
+                // scanless canonical would turn the pre filter into a no-op 
for every group
+                registerRelationKey(structCanonical);
+            }
+            // a SET after a failed DELETE re-creates the entry: drop the 
deletion intent so a
+            // pending load does not skip the re-created row
+            pendingLoadTombstones.remove(fingerprint);
+            // persist under the same lock as DELETE's DB removal, so 
concurrent same-key
+            // SET/DELETE serialize their DB writes in the same order as their 
memory updates
+            // and, when the best-effort writes succeed, the stored row 
matches the last memory
+            // writer (a failed write only logs a warning and may reappear 
after a FE restart)
+            if (persistenceEnabled()) {
+                HboStatisticsStore.persist(fingerprint, rows, type, 
structCanonical, expansion, literalMode,
+                        createTimeMs);
+            }
+        }
+    }
+
+    /**
+     * The pinned row-count entry of a sub tree. Entries of another kind (a 
join expansion factor
+     * keyed by the join conditions) are never returned here, so the two read 
paths can not pick up
+     * each other's entry.
+     *
+     * <p>This is the planning hot path, so it only reads the in-memory cache: 
the persisted entries
+     * of this FE are loaded by {@link HboPinnedStatisticsLoader} in the 
background. Doing it here
+     * would run a DDL and a full table scan on a user query's planning 
thread, and the internal
+     * statement it runs would re-enter this manager (see the class javadoc).
+     */
+    public Optional<PinnedHboStatistics> getPinnedPlanStatistics(String 
fingerprint) {
+        return 
Optional.ofNullable(pinnedPlanStatistics.getIfPresent(fingerprint))
+                .filter(pinned -> !pinned.isExpansion());
+    }
+
+    /**
+     * Whether this FE holds any pinned entry at all (of any kind). The 
planning path looks an entry
+     * up by the hbo fingerprint of a plan node, and that fingerprint is 
expensive to build (a whole
+     * memo subtree traversal); an empty cache means every such lookup is a 
guaranteed miss, so the
+     * caller can skip building the fingerprint. Like {@link 
#getPinnedPlanStatistics(String)} this
+     * only reads the in-memory cache and never triggers the background loader.
+     */
+    public boolean hasAnyPinnedStatistics() {
+        return !pinnedPlanStatistics.asMap().isEmpty();
+    }
+
+    /**
+     * Whether a pinned entry could possibly apply to a sub tree which reads 
exactly {@code
+     * relationKey} (see {@link 
GroupStructInfo.HboStructSummary#getRelationKey()}).
+     *
+     * <p>An entry is keyed by the fingerprint of its struct info, and two 
equal struct infos
+     * necessarily read the same tables, so an entry whose relation key 
differs can not match: the
+     * caller can skip building the (expensive) fingerprint. The index is an 
over approximation -
+     * keys are only ever added, so a removed or evicted entry may keep its 
key - which can only make
+     * a lookup happen, never suppress one.
+     */
+    public boolean mayHavePinnedEntryForRelations(String relationKey) {
+        return pinnedEntriesWithoutRelationKey.get() > 0 || 
pinnedRelationKeys.contains(relationKey);
+    }
+
+    /** Register the relation key of an entry's struct info in the lookup 
index. */
+    private void registerRelationKey(String structCanonical) {
+        List<HboScanDescriptor> scans = 
HboScanDescriptor.parseAll(structCanonical);
+        if (scans.isEmpty()) {
+            // an entry which records no scan at all can not be filtered by a 
relation key
+            pinnedEntriesWithoutRelationKey.incrementAndGet();
+            return;
+        }
+        List<String> tables = new ArrayList<>(scans.size());
+        for (HboScanDescriptor scan : scans) {
+            tables.add(scan.getTable());
+        }
+        Collections.sort(tables);
+        pinnedRelationKeys.add(String.join(",", tables));
+    }
+
+    /**
+     * Remove a pinned statistics entry. Learned entries of the same key (if 
any) are left
+     * untouched here; callers may invalidate them explicitly.
+     */
+    public void removePinnedPlanStatistics(String fingerprint) {
+        synchronized (pinnedLoadLock) {
+            pinnedPlanStatistics.invalidate(fingerprint);
+            if (persistenceEnabled()) {
+                // the DB removal runs under the lock so a lazy load can never 
SELECT the row in
+                // the window between the memory invalidation and its DB 
removal (M-1 R1)
+                if (!HboStatisticsStore.delete(fingerprint) && 
!hboPinnedLoaded) {
+                    // the row is still stored and a pending/retrying load 
would resurrect it
+                    // into memory: remember the deletion intent (M-1 R2)
+                    pendingLoadTombstones.add(fingerprint);
+                }
+            }
+        }
+    }
+
+    /**
+     * Inject (or overwrite) the fan-out factor of a set of join equality 
conditions
+     * ({@code HBO SET STATISTICS ... TYPE=JOIN_EXPANSION}). It is a pinned 
entry like every other
+     * hbo statistics entry - same table, same lifecycle - only its value is a 
factor instead of a
+     * row count and its key is the join condition fingerprint.
+     *
+     * @param fingerprint sha256 of the canonical equality-condition set
+     * @param expansion measured fan-out factor relative to the larger input 
of the join (0.1 keeps
+     *                  10% of the larger input, 1000 fans out to 1000 times 
it), greater than 0
+     * @param condCanonical the canonical equality-condition set the factor 
was measured for
+     */
+    public void putPinnedExpansionStatistics(String fingerprint, double 
expansion, String condCanonical) {
+        // a condition key never carries a literal by itself: it is the 
constant agnostic form
+        putPinnedStatistics(fingerprint, 0, PinnedType.JOIN_EXPANSION, 
condCanonical, expansion,
+                LiteralMode.NO_LITERAL);
+    }
+
+    /**
+     * The pinned join expansion entry of a set of equality conditions, if one 
was injected.
+     */
+    public Optional<PinnedHboStatistics> getPinnedExpansion(String 
fingerprint) {
+        return 
Optional.ofNullable(pinnedPlanStatistics.getIfPresent(fingerprint))
+                .filter(PinnedHboStatistics::isExpansion);
+    }
+
+    /**
+     * Inject a learned entry (used by {@code HBO SET LEARNED STATISTICS}) so 
that the learned
+     * lookup path can be exercised without waiting for a real profile 
publish. The injected entry
+     * carries no input table statistics and therefore matches by fingerprint 
alone.
+     */
+    public void putLearnedPlanStatistics(String fingerprint, long rows, String 
structCanonical) {
+        PlanStatistics planStatistics = new PlanStatistics(0, rows, rows, 
rows, rows,
+                rows, rows, rows, rows, 0, 0, 1);
+        RecentRunsPlanStatistics runs = new RecentRunsPlanStatistics(
+                Lists.newArrayList(new 
RecentRunsPlanStatisticsEntry(planStatistics, Lists.newArrayList())));
+        hboPlanStatisticsProvider.putHboPlanStatsByFingerprint(fingerprint, 
runs);
+        if (structCanonical != null && !structCanonical.isEmpty()) {

Review Comment:
   认可,已修复(0cbdc52d362):SET LEARNED 未带 STRUCT 时现在会 invalidate 掉旧条目的 
learnedStructCanonical,SHOW 不会再用新行数配旧 Struct Info / STATE / LIKE 匹配。



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/HboShowStatisticsCommand.java:
##########
@@ -0,0 +1,275 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.RedirectStatus;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.stats.HboPlanStatisticsManager;
+import 
org.apache.doris.nereids.stats.HboPlanStatisticsManager.PinnedHboStatistics;
+import org.apache.doris.nereids.stats.HboStructFreshness;
+import org.apache.doris.nereids.stats.SimpleStructInfo;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.statistics.hbo.RecentRunsPlanStatistics;
+import org.apache.doris.statistics.hbo.RecentRunsPlanStatisticsEntry;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Manual HBO statistics inspection:
+ * <pre>
+ *   HBO SHOW STATISTICS [LIKE '&lt;pattern&gt;']              -- pinned + 
learned, simplified struct info
+ *   HBO SHOW STATISTICS FULL [LIKE '&lt;pattern&gt;']         -- canonical 
struct info
+ *   HBO SHOW PINNED STATISTICS [FULL] [LIKE '&lt;pattern&gt;']   -- injected 
entries only
+ *   HBO SHOW LEARNED STATISTICS [FULL] [LIKE '&lt;pattern&gt;']  -- learned 
entries only
+ * </pre>
+ * {@code FULL} decides both which struct info is printed and which one {@code 
LIKE} matches (the
+ * canonical form, i.e. the fingerprint input, or its simplified rendering). 
The simplified form is
+ * a pure function of the canonical form (see {@link SimpleStructInfo}), so it 
is never persisted and
+ * can never be stale.
+ *
+ * <p>Pinned entries come from the in-memory (authoritative) pinned cache of 
this FE; learned entries
+ * come from the recent-runs cache keyed by hbo fingerprint. The learned row 
count is the output
+ * row count of the latest recorded run of that fingerprint (see the Detail 
column for the number of
+ * recorded runs), so it is a summary, not a single current value.
+ *
+ * <p><b>The State column is judged in catalog scope.</b> This statement 
cannot run the query an
+ * entry was injected for, so it compares the recorded data state with the 
tables as a whole
+ * ({@code HboStructFreshness.of}): an entry whose struct prunes partitions 
reports {@code unknown}
+ * here even while the read side keeps applying it for a query which reads the 
partitions it was
+ * measured on. The state the read side actually saw is the {@code last=} part 
of the Detail column
+ * (filled when this FE applied the entry) and the {@code used=} / {@code 
skipped=} marker of the
+ * EXPLAIN annotation. {@code HBO DELETE STALE STATISTICS} removes on this 
catalog scope as well, so
+ * it can only remove an entry which is stale for the tables as a whole.
+ */
+public class HboShowStatisticsCommand extends ShowCommand {
+    private static final String SCOPE_PINNED = "pinned";
+    private static final String SCOPE_LEARNED = "learned";
+
+    private final String scope;
+    private final boolean fullStructInfo;
+    private final String likePattern;
+    private final ShowResultSetMetaData metaData;
+
+    /**
+     * HboShowStatisticsCommand
+     * @param scope PINNED / LEARNED, null or empty means both
+     * @param fullStructInfo print (and match against) the canonical struct 
info instead of the
+     *                       simplified one
+     * @param likePattern optional SQL LIKE pattern applied to the printed 
struct info column
+     */
+    public HboShowStatisticsCommand(String scope, boolean fullStructInfo, 
String likePattern) {
+        super(PlanType.HBO_SHOW_STATISTICS_COMMAND);
+        this.scope = scope == null || scope.isEmpty() ? null : 
scope.toLowerCase(Locale.ROOT);
+        this.fullStructInfo = fullStructInfo;
+        this.likePattern = likePattern == null || likePattern.isEmpty() ? null 
: likePattern;
+        this.metaData = ShowResultSetMetaData.builder()
+                .addColumn(new Column("Kind", ScalarType.createVarchar(16)))
+                .addColumn(new Column("Fingerprint", 
ScalarType.createVarchar(64)))
+                .addColumn(new Column("LiteralMode", 
ScalarType.createVarchar(16)))
+                .addColumn(new Column("Type", ScalarType.createVarchar(16)))
+                .addColumn(new Column("Value", ScalarType.createVarchar(32)))
+                .addColumn(new Column(fullStructInfo ? "StructInfo" : 
"SimpleStruct",
+                        ScalarType.createVarchar(65533)))
+                .addColumn(new Column("Baseline", 
ScalarType.createVarchar(512)))
+                .addColumn(new Column("State", ScalarType.createVarchar(16)))
+                .addColumn(new Column("Detail", ScalarType.createVarchar(512)))
+                .build();
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        if (!Env.getCurrentEnv().getAccessManager()
+                .checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) {
+            throw new AnalysisException("Access denied: HBO statistics 
management requires ADMIN privilege");
+        }
+        if (scope != null && !SCOPE_PINNED.equals(scope) && 
!SCOPE_LEARNED.equals(scope)) {
+            throw new AnalysisException("invalid hbo show scope, expect PINNED 
or LEARNED: " + scope);
+        }
+        Pattern pattern = compileLikePattern(likePattern);
+        HboPlanStatisticsManager hboManager = 
Env.getCurrentEnv().getHboPlanStatisticsManager();
+        List<List<String>> rows = new ArrayList<>();
+        if (scope == null || SCOPE_PINNED.equals(scope)) {
+            for (PinnedHboStatistics pinned : 
hboManager.getAllPinnedPlanStatistics().values()) {
+                String structInfo = 
displayedStructInfo(pinned.getStructCanonical());
+                if (!matches(pattern, pinned.getStructCanonical(), 
structInfo)) {
+                    continue;
+                }
+                // an expansion entry is keyed by join conditions, so it is 
not tied to any data
+                // state and can not go stale
+                HboStructFreshness freshness = pinned.isExpansion()
+                        ? null : 
HboStructFreshness.of(pinned.getStructCanonical());
+                List<String> row = new ArrayList<>();
+                row.add(SCOPE_PINNED);
+                row.add(pinned.getFingerprint());
+                
row.add(pinned.getLiteralMode().name().toLowerCase(Locale.ROOT));
+                row.add(pinned.getType().name().toLowerCase(Locale.ROOT));
+                // a JOIN_EXPANSION entry carries a fan-out factor instead of 
a row count; the x
+                // suffix keeps the two readable in one column
+                row.add(pinned.isExpansion()
+                        ? trimDouble(pinned.getExpansion()) + "x" : 
String.valueOf(pinned.getRows()));
+                row.add(structInfo);
+                row.add(freshness == null ? "-" : freshness.getRecorded());
+                row.add(freshness == null ? "-" : freshness.getState());
+                row.add(pinnedDetail(pinned.getCreateTime(),
+                        freshness == null ? null : freshness.getLiveDetail(), 
pinned.getLastUse()));
+                rows.add(row);
+            }
+        }
+        if (scope == null || SCOPE_LEARNED.equals(scope)) {
+            for (Map.Entry<String, RecentRunsPlanStatistics> entry : 
hboManager.getHboPlanStatisticsProvider()
+                    .getAllHboPlanStats().entrySet()) {
+                Optional<String> canonical = 
hboManager.getLearnedStructCanonical(entry.getKey());
+                // a learned key is generated internally, so it only has a 
struct info when it was
+                // injected together with a struct literal
+                String structInfo = 
canonical.map(this::displayedStructInfo).orElse("");
+                if (!matches(pattern, canonical.orElse(null), structInfo)) {
+                    continue;
+                }
+                List<RecentRunsPlanStatisticsEntry> recentRuns = 
entry.getValue().getRecentRunsStatistics();
+                String rowsText = recentRuns.isEmpty() ? ""
+                        : String.valueOf(recentRuns.get(recentRuns.size() - 
1).getPlanStatistics().getOutputRows());
+                List<String> row = new ArrayList<>();
+                row.add(SCOPE_LEARNED);
+                row.add(entry.getKey());
+                // learned keys are constant agnostic for join / aggregation 
and the scan token for
+                // scans, so no user facing granularity can be reported
+                row.add("-");
+                row.add("-");
+                row.add(rowsText);
+                row.add(structInfo.isEmpty() ? "-" : structInfo);
+                HboStructFreshness freshness = 
canonical.map(HboStructFreshness::of).orElse(null);
+                row.add(freshness == null ? "-" : freshness.getRecorded());
+                row.add(freshness == null ? HboStructFreshness.STATE_UNKNOWN : 
freshness.getState());
+                row.add(freshness == null || freshness.getLive().isEmpty()
+                        ? "runs=" + recentRuns.size()
+                        : "runs=" + recentRuns.size() + " now=" + 
freshness.getLiveDetail());
+                rows.add(row);
+            }
+        }
+        rows.sort(Comparator.comparing((List<String> row) -> row.get(0))
+                .thenComparing(row -> row.get(1)));
+        return new ShowResultSet(metaData, rows);
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        return metaData;
+    }
+
+    @Override
+    public RedirectStatus toRedirectStatus() {
+        return RedirectStatus.FORWARD_NO_SYNC;
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitCommand(this, context);
+    }
+
+    /**
+     * The Detail column: when the entry was created, the data state it is 
compared with now, and
+     * when a query of this FE last applied it (with the state it was applied 
in). The {@code last}
+     * part is the only one which comes from the read side: it describes what 
the entry was actually
+     * applied against, while {@code now} is read from the catalog here (see
+     * {@link HboStructFreshness} for why the two can differ).
+     */
+    private static String pinnedDetail(long createTime, String liveDetail, 
String lastUse) {
+        StringBuilder detail = new StringBuilder("created=").append(
+                
TimeUtils.getDatetimeFormatWithTimeZone().format(LocalDateTime.ofInstant(
+                        Instant.ofEpochMilli(createTime), 
ZoneId.systemDefault())));
+        if (liveDetail != null && !liveDetail.isEmpty() && 
!"-".equals(liveDetail)) {
+            detail.append(",now=").append(liveDetail);
+        }
+        if (lastUse != null && !lastUse.isEmpty()) {
+            detail.append(",last=").append(lastUse);
+        }
+        return detail.toString();
+    }
+
+    private String displayedStructInfo(String canonicalStructInfo) {
+        return fullStructInfo ? canonicalStructInfo : 
SimpleStructInfo.render(canonicalStructInfo);
+    }
+
+    /**
+     * {@code LIKE} matches the printed struct info column. An entry without a 
struct info (a learned
+     * entry injected by fingerprint only) cannot match a struct info pattern.
+     */
+    private boolean matches(Pattern pattern, String canonicalStructInfo, 
String displayedStructInfo) {
+        if (pattern == null) {
+            return true;
+        }
+        return canonicalStructInfo != null && !displayedStructInfo.isEmpty()
+                && pattern.matcher(displayedStructInfo).matches();
+    }
+
+    /** Render a fan-out factor without a trailing {@code .0}. */
+    private static String trimDouble(double value) {
+        return value == Math.floor(value) && !Double.isInfinite(value)
+                ? String.valueOf((long) value) : String.valueOf(value);

Review Comment:
   认可,已修复(0cbdc52d362):trimDouble 只在值位于 long 范围内且为整数时才转 long,超出范围保留 double 
形式(1.0E20),不再显示成 9223372036854775807。



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/stats/HboStructFreshness.java:
##########
@@ -0,0 +1,606 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.stats;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.Config;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.statistics.hbo.PlanStatistics;
+import org.apache.doris.statistics.hbo.ScanPlanStatistics;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Whether a hbo entry may still be applied: the recorded data state of an 
entry (the baseline of
+ * every scan of its struct info, see {@link HboScanDescriptor}) compared with 
the data state of the
+ * query which wants to use it.
+ *
+ * <p>A hbo fingerprint does not contain any data state any more, so an entry 
keeps matching while a
+ * table grows. The read side therefore has to decide whether the injected row 
count still describes
+ * the data it was measured on:
+ * <ul>
+ *   <li>{@link #STATE_LIVE}: the data the entry was measured on is still the 
current one, the entry
+ *       is applied;</li>
+ *   <li>{@link #STATE_DRIFTED}: the data moved, but by at most
+ *       {@code Config.hbo_row_count_change_ratio} of the recorded row count 
(10% by default): the
+ *       entry is applied, which is the point of the tolerance - a table which 
merely keeps growing
+ *       (e.g. a few new rows in a partition) does not invalidate a measured 
row count;</li>
+ *   <li>{@link #STATE_STALE}: the data moved further than that: the entry is 
<b>not</b> applied, and
+ *       the query falls back to the optimizer estimation;</li>
+ *   <li>{@link #STATE_UNKNOWN}: nothing can be compared (the entry records no 
row count, or the
+ *       current one cannot be read / was obtained a different way), so there 
is nothing to judge: the
+ *       entry is applied as it always was.</li>
+ * </ul>
+ * The recorded row count is never scaled: an entry either applies with the 
value the user measured
+ * or does not apply at all ("soft strategy" - a user who needs the exact 
recorded state in any case
+ * should not inject hbo statistics for that query). Setting
+ * {@code hbo_row_count_change_ratio} to 0 or less switches the verdict back 
to the strict semantics
+ * hbo had before the baseline existed: the recorded visible version has to 
still be the current one.
+ *
+ * <p>The rows are compared <b>before</b> the version, and only against a 
number which was obtained
+ * the same way ({@link HboScanDescriptor.RowsKind}): the visible version is a 
property of the table,
+ * so two different partition selections of one table share it while reading 
completely different
+ * amounts of data, and a measured row count must not be compared with one 
that was derived from the
+ * table average. Whenever nothing can be compared - a hand written struct 
literal, an entry whose
+ * rows were never measured, a current row count which cannot be read, or two 
numbers of different
+ * kinds - the state is {@link #STATE_UNKNOWN} and the entry is applied: a 
transient failure to read
+ * the catalog must not drop an entry the user injected. The version is only 
decisive in strict mode
+ * ({@code hbo_row_count_change_ratio <= 0}).
+ *
+ * <p>A scan is judged by the rows of its <b>selected</b> partitions, so 
pruning decides which data
+ * the entry depends on: an unrelated partition which grows does not 
invalidate a pruned entry.
+ * {@code HBO SHOW STATISTICS} and {@code HBO DELETE STALE STATISTICS} cannot 
run the query of an
+ * entry, so there the recorded state is compared with the table as a whole; 
an entry whose struct
+ * prunes partitions can therefore only be judged by rows when it was recorded 
as a whole table scan
+ * (its state is reported as unknown otherwise, and such an entry is kept 
unless the user asks for
+ * {@code OLDER_THAN}).
+ *
+ * <p>Learned (profile collected) entries carry the input table statistics of 
the run they were
+ * measured in, so they are judged by the same tolerance over those recorded 
rows; an entry injected
+ * by {@code HBO SET LEARNED STATISTICS} carries no such statistics and is 
applied as it always was.
+ */
+public class HboStructFreshness {
+    /** The recorded data state is still the current one. */
+    public static final String STATE_LIVE = "live";
+    /** The data moved, but within the tolerance of {@code 
Config.hbo_row_count_change_ratio}. */
+    public static final String STATE_DRIFTED = "drifted";
+    /** The data moved further than the tolerance: the entry must not be 
applied. */
+    public static final String STATE_STALE = "stale";
+    /** The entry records no data state (or it cannot be read any more): 
nothing to judge. */
+    public static final String STATE_UNKNOWN = "unknown";
+
+    private static final Logger LOG = 
LogManager.getLogger(HboStructFreshness.class);
+
+    private final String state;
+    // the recorded baseline and the current state, both rendered for display
+    private final String recorded;
+    private final String live;
+    // the row counts of the scan which decided the state, {@link 
HboScanDescriptor#UNKNOWN} when the
+    // state was decided by something else (e.g. a version which is not 
readable)
+    private final long recordedRows;
+    private final long liveRows;
+
+    private HboStructFreshness(String state, String recorded, String live, 
long recordedRows, long liveRows) {
+        this.state = state;
+        this.recorded = recorded;
+        this.live = live;
+        this.recordedRows = recordedRows;
+        this.liveRows = liveRows;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public boolean isStale() {
+        return STATE_STALE.equals(state);
+    }
+
+    public boolean isUnknown() {
+        return STATE_UNKNOWN.equals(state);
+    }
+
+    public boolean isDrifted() {
+        return STATE_DRIFTED.equals(state);
+    }
+
+    /** The data state the entry was recorded in, e.g. {@code 
hbo_test.t1:v3,r1000,p1/5}. */
+    public String getRecorded() {
+        return recorded;
+    }
+
+    /** The data state the entry is compared with (empty when it cannot be 
read). */
+    public String getLive() {
+        return live;
+    }
+
+    /**
+     * The verdict as one token, printed after {@code used=} / {@code 
skipped=} in the explain
+     * annotation, e.g. {@code drifted(rows=1030,rec=1000,+3.0%)}.
+     */
+    public String getSummary() {
+        if (!STATE_DRIFTED.equals(state) && !STATE_STALE.equals(state)) {
+            return state;
+        }
+        if (recordedRows == HboScanDescriptor.UNKNOWN || liveRows == 
HboScanDescriptor.UNKNOWN) {
+            return state;
+        }
+        return state + "(rows=" + liveRows + ",rec=" + recordedRows + ","
+                + changeText(recordedRows, liveRows) + ")";
+    }
+
+    /** The {@code now} part of {@code HBO SHOW STATISTICS}: the state read 
from the catalog. */
+    public String getLiveDetail() {
+        if (live.isEmpty()) {
+            return "-";
+        }
+        if (recordedRows == HboScanDescriptor.UNKNOWN || liveRows == 
HboScanDescriptor.UNKNOWN) {
+            return live;
+        }
+        return live + "," + changeText(recordedRows, liveRows);
+    }
+
+    /**
+     * Whether the entry records any data state at all. An entry which does 
not (a struct literal
+     * typed by hand, or one copied from a plan whose tables were never 
reported) is always applied,
+     * so a caller does not have to compute the state of the current query to 
find that out.
+     */
+    public static boolean hasRecordedDataState(String recordedCanonical) {
+        for (HboScanDescriptor scan : 
HboScanDescriptor.parseAll(recordedCanonical)) {
+            if (scan.hasVisibleVersion() || scan.hasScanRows()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Judge an entry against the current query: the data state recorded in 
{@code recordedCanonical}
+     * (the struct info of the entry) against the state of {@code 
liveCanonical} (the struct info the
+     * read side just computed for the group). This is the decision of the 
read side.
+     */
+    public static HboStructFreshness between(String recordedCanonical, 
GroupStructInfo liveStructInfo) {
+        return between(recordedCanonical, liveStructInfo.getScans());
+    }
+
+    /** The same comparison for a caller which has the scans of the current 
query only. */
+    static HboStructFreshness between(String recordedCanonical, 
List<HboScanDescriptor> liveScans) {
+        List<HboScanDescriptor> recordedScans = 
HboScanDescriptor.parseAll(recordedCanonical);
+        String liveText = renderScans(liveScans);
+        if (recordedScans.isEmpty()) {
+            // an entry without a baseline (a hand written struct literal): 
nothing to judge
+            return new HboStructFreshness(STATE_UNKNOWN, "", liveText,
+                    HboScanDescriptor.UNKNOWN, HboScanDescriptor.UNKNOWN);
+        }
+        if (recordedScans.size() != liveScans.size()) {
+            // the entry describes a different number of scans than the group: 
cannot be compared
+            return new HboStructFreshness(STATE_UNKNOWN, 
renderScans(recordedScans), liveText,
+                    HboScanDescriptor.UNKNOWN, HboScanDescriptor.UNKNOWN);
+        }
+        String state = STATE_LIVE;
+        String recordedText = renderScans(recordedScans);
+
+        long recordedRows = HboScanDescriptor.UNKNOWN;
+        long liveRows = HboScanDescriptor.UNKNOWN;
+        for (int i = 0; i < recordedScans.size(); i++) {
+            HboScanDescriptor recorded = recordedScans.get(i);
+            HboScanDescriptor live = liveScans.get(i);
+            String scanState;
+            if (!recorded.getTable().equals(live.getTable())) {
+                // the same fingerprint describes the same scans in the same 
order, so this cannot
+                // happen for an entry of this group: refuse to judge rather 
than guess
+                scanState = STATE_UNKNOWN;
+            } else {
+                scanState = stateOf(recorded, live);
+            }
+            if (worse(scanState, state).equals(scanState)) {
+                state = scanState;
+                recordedRows = recorded.getScanRows();
+                liveRows = live.getScanRows();
+            }
+        }
+        return new HboStructFreshness(state, recordedText, liveText, 
recordedRows, liveRows);
+    }
+
+    /**
+     * Judge an entry against the catalog as a whole (used by {@code HBO SHOW 
STATISTICS} and
+     * {@code HBO DELETE STALE STATISTICS}, which cannot run the query of an 
entry): the recorded
+     * visible version and row count of every table against the current ones.
+     *
+     * <p>Row counts are only comparable when the scan read the whole table 
(no partition was pruned
+     * away), because only then the recorded rows are the rows of the table.
+     */
+    public static HboStructFreshness of(String canonicalStructInfo) {
+        List<HboScanDescriptor> recordedScans = 
HboScanDescriptor.parseAll(canonicalStructInfo);
+        if (recordedScans.isEmpty()) {
+            return new HboStructFreshness(STATE_UNKNOWN, "", "", 
HboScanDescriptor.UNKNOWN,
+                    HboScanDescriptor.UNKNOWN);
+        }
+        String state = STATE_LIVE;
+        // the current state of every table, de duplicated like the recorded 
side (a self join reads
+        // the same table twice, which would only repeat the same numbers)
+        Set<String> liveTexts = new LinkedHashSet<>();
+        long recordedRows = HboScanDescriptor.UNKNOWN;
+        long liveRows = HboScanDescriptor.UNKNOWN;
+        for (HboScanDescriptor recorded : recordedScans) {
+            OlapTable table = resolveTable(recorded.getTable());
+            if (table == null) {
+                if (worse(STATE_UNKNOWN, state).equals(STATE_UNKNOWN)) {
+                    state = STATE_UNKNOWN;
+                }
+                liveTexts.add(recorded.getTable() + ":-");
+                continue;
+            }
+            if (table.getIndexIdList().size() > 1 && 
!recorded.isPartitionSelectionComplete()) {
+                // the table has more than one index (a rollup) and the entry 
records only a partition
+                // count, so this statement cannot tell which index its row 
count came from: judging it
+                // against the base index could be wrong in both directions, 
so it is left to a query
+                liveTexts.add(recorded.getTable() + ":-");
+                if (worse(STATE_UNKNOWN, state).equals(STATE_UNKNOWN)) {
+                    state = STATE_UNKNOWN;
+                }
+                continue;
+            }
+            long currentVersion = visibleVersionOf(table);
+            // the rows of the whole table, computed the same way the recorded 
rows of a whole table
+            // scan were computed, so the two numbers describe the same thing
+            HboScanDescriptor.Rows liveRowsOfTable = 
HboScanDescriptor.rowsOf(table,
+                    new ArrayList<>(table.getPartitionIds()), 
table.getBaseIndexId());
+            long currentRows = liveRowsOfTable.getRows();
+            HboScanDescriptor.RowsKind currentRowsKind = 
liveRowsOfTable.getKind();
+            liveTexts.add(recorded.getTable() + ":v" + currentVersion
+                    + (currentRows == HboScanDescriptor.UNKNOWN ? ""
+                            : (currentRowsKind == 
HboScanDescriptor.RowsKind.MEASURED ? ",r" : ",e")
+                                    + currentRows));
+            String scanState;
+            if (!recorded.hasVisibleVersion() && !recorded.hasScanRows()) {
+                // nothing was recorded for this scan: it applies as it always 
was, nothing to judge
+                scanState = STATE_UNKNOWN;
+                currentRows = HboScanDescriptor.UNKNOWN;
+            } else if (threshold() <= 0) {
+                if (currentVersion == HboScanDescriptor.UNKNOWN || 
!recorded.hasVisibleVersion()) {
+                    // a temporary metadata failure must not look like a 
version mismatch: DELETE
+                    // STALE would remove an entry which a query can still 
apply
+                    scanState = STATE_UNKNOWN;
+                } else {
+                    scanState = currentVersion == recorded.getVisibleVersion() 
? STATE_LIVE : STATE_STALE;
+                }
+                currentRows = HboScanDescriptor.UNKNOWN;
+            } else if (recorded.hasVisibleVersion() && currentVersion == 
recorded.getVisibleVersion()) {

Review Comment:
   认可,已修复(0cbdc52d362):catalog 侧的“版本未变即 live”捷径现在只在本轮行数不可比较(未知或 kind 不同)时生效;同 
kind 行数一律走与读侧一致的行数比较,因此 e1000 -> e50000(ALTER COLUMN STATS ... 
ROW_COUNT)在版本未变时也会被判为 stale。



-- 
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