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 '<pattern>'] -- pinned +
learned, simplified struct info
+ * HBO SHOW STATISTICS FULL [LIKE '<pattern>'] -- canonical
struct info
+ * HBO SHOW PINNED STATISTICS [FULL] [LIKE '<pattern>'] -- injected
entries only
+ * HBO SHOW LEARNED STATISTICS [FULL] [LIKE '<pattern>'] -- 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]