Repository navigation
Conversation
…struct-info keys
### What problem does this solve?
The nereids optimizer estimates a plan from catalog statistics alone, so a query whose data distribution
or predicate selectivity the catalog cannot describe (a skewed join key, a selective range, a filter
whose matching rows are known from previous executions) is estimated from heuristics. This change adds
a history-based optimizer (HBO): the statistics a qualifying execution actually observed are published
as learned entries and reused by the next query whose plan is the same, and an administrator can pin a
value for a plan node.
A plan is identified by the struct info of its memo group: a tree description of the node kinds, the
normalized expressions, the scan identities and the children. Two literal modes are keyed - the exact
one and the constant agnostic one which folds the literals of a predicate - and the key is the sha256 of
the structural identity of that tree, not of its printed form, so a table name, an alias or a literal
which carries a character of the printed grammar cannot stand for another field. The scan token records
the table id, the relation alias, the index and the selected partitions, which is what turns a
constant-agnostic key into an entry which is only applied to the same data state.
The read side matches a learned entry by its fingerprint and then validates it against the input table
statistics the entry recorded (rows read per scan) and against the current data state (visible version,
row count, partitions), so a stale or drifted entry is not applied. A published entry carries the
predicates of the scans it measured, the grouping definitions and the join conditions, so two queries
which differ in a predicate the key folds cannot share a count. Manual management is served by
`HBO SET/SHOW/DELETE STATISTICS` (pinned entries), `HBO SET LEARNED STATISTICS` (tests and experts),
`hbo_persist_pinned_to_internal_db` (pinned entries survive a restart) and `SET show_hbo_fingerprint`
(EXPLAIN prints the key and the struct of every node, with the verdict of the pinned and learned
entries). Regression suites cover injection, drift, learned reuse, the legacy configs and persistence.
### Release note
Adds history-based optimizer statistics: learned statistics are published after qualifying executions
and reused by later queries with the same plan, pinned statistics can be managed with `HBO ... STATISTICS`
statements and persisted to `__internal_schema`, and `EXPLAIN` can print the plan key together with the
statistics which were applied.
### Check List (For Author)
- Test: Unit Test + Regression test
- Unit: the `org.apache.doris.nereids.stats` package (struct-info golden corpus: 76 plan cases and
51 text cases, both regenerated and asserted; scan identity, freshness, guards, pinned race,
expansion keys, input merge) plus the HBO command suites, 200 cases pass.
- Regression: a FE rebuilt from this branch runs `query_p0/hbo` to its baseline (19 suites, the 8
`legacy/*` suites fail because the environment does not support them); the injection, drift,
learned, persistence and display suites pass.
- Behavior changed: Yes - the optimizer reuses history for a matched plan, and EXPLAIN can print the
key of a node with `SET show_hbo_fingerprint=true`.
- Does this need documentation: Yes - the HBO statements, the session variables and the internal table
need a manual (the PR body of the squashed change describes the semantics).
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review of all supplied changed paths at 1322847. Eight new P2 findings cover incomplete semantic keys, cross-table inner-join expansion collisions, selective/zero input baselines, strict learned freshness, stale skew costing, and learned placeholder replacement. No existing inline findings were supplied. No additional user focus was specified.
Critical checkpoint conclusions:
- Goal and evidence: HBO identity, feedback reuse and manual management are implemented, but the inline findings prevent reliable reuse in the concrete plans described. Added tests cover many identity and injection cases; they do not establish automatic feedback reuse.
- Scope and reuse: the broad change is related to HBO; group descriptions and providers centralize the main lifecycle. Semantic fields and parallel consumers need consistent treatment.
- Concurrency: planner, profile, admin, config and loader threads share caches. Pinned mutation/load operations serialize on pinnedLoadLock, including internal SQL. Planning reads concurrent caches without acquiring that lock; no concrete deadlock was found.
- Lifecycle: per-query snapshots and cache expiry bound retained feedback. Loader restore is one-time and best effort per FE. The learned-placeholder replacement defect is described inline; no native/static initialization issue applies.
- Configuration: mutable ratio, guard and persistence settings are read on use; pinned cache capacity is explicitly startup-only. Strict learned-version behavior is defective as reported.
- Compatibility: no new FE-BE wire field or user-data format changes. Old learned keys cold-miss; internal pinned table columns are checked and old schemas explicitly require recreation.
- Parallel paths: filters, joins, aggregates, Java/Python UDF metadata and physical reconstruction were checked. Broadcast skew costing lacks the freshness fence used by row-count lookup. Mark-join semantics require a distinct key.
- Conditional checks: unsupported CTE consumers, table sampling, external feedback and physical-only partial stages are deliberately excluded; no duplicate local/final aggregate issue was verified.
- Tests: unit/golden and regression coverage was inspected, including negative command/guard cases. Real profile publication/reuse, restart loading and concurrent internal SQL lack end-to-end coverage. No builds or tests were run under this static-only contract.
- Test results: the added persistence output agrees with its injected values and singleton queries. Eight legacy suites are unchanged-content renames. Output-generation provenance was not independently executed.
- Observability: EXPLAIN applied/skipped annotations and persistence/load/snapshot logs support diagnosis; no separate logging defect was verified.
- Persistence and failover: pinned records use internal OLAP SQL, with checked execution errors, generation-bounded deletes and pending-load tombstones; cross-FE live refresh is explicitly unsupported. Restart/failover behavior remains unexecuted.
- Data writes and atomicity: no user-data write or transaction visibility/delete-bitmap path changes. Pinned memory operations are serialized and database persistence is declared best effort; no additional concrete atomicity bug was found.
- FE-BE propagation: existing runtime counters/statistics transport are reused; the counter-to-population mismatch is reported inline.
- Performance and memory: caches, scan-width gating and relation prefilters limit common-path work. Incorrect estimate reuse/misses and stale skew discounts can regress plans; no additional substantiated performance issue was found.
- Other review concerns: all candidates and initial risks have explicit dispositions in the shared ledger. Findings concern estimates, costing and feature behavior; no wrong-result claim is made.
Convergence is recorded separately after reviewer validation of the frozen findings. Validation limits above describe static review scope, not executed runtime results.
Review converged after 3 round(s).
| // arguments keep a different number of rows in another time zone | ||
| return className + '<' + encodeSlotComponent(sessionTimeZone()) + '>'; | ||
| } | ||
| return className; |
There was a problem hiding this comment.
[P2] Preserve semantic fields in normalized expressions
Aggregate(gb=CAST(t.s AS DECIMAL(10,0))) -> Scan(t) and the same plan with DECIMAL(10,1) both encode A{gb:Cast(col(t.s))}. For string values '1.1' and '1.2' these have one versus two groups. Cast.targetType is outside its children, so this fallback drops it in both literal modes; computeAggregate can then reuse the other query's learned or pinned count while identical scans pass freshness. The same omission drops OrderExpression direction: HAVING group_concat(v ORDER BY v ASC)='a,b' and DESC share an exact key even though only one group passes. Include cast targets and order direction/null ordering in expression identity, and cover these paired plans.
There was a problem hiding this comment.
不需要. 出现cast 绝大部分都是因为 两个decimal字段的 scale/precision不匹配.如果cast的type不同,说明参与计算的两个字段至少一个不同. 所以两个hbo条目本身就不可能匹配
| } | ||
| Map<String, String> tables = new TreeMap<>(); | ||
| Plan joinPlan = (Plan) join; | ||
| List<Plan> relations = joinPlan.collectToList(planNode -> planNode instanceof CatalogRelation); |
There was a problem hiding this comment.
[P2] Include the bound tables in inner-join expansion keys
FROM t1 a JOIN t2 b ON a.k=b.k and FROM t3 a JOIN t4 b ON a.k=b.k in the same database retain the same alias-qualified condition, even after alias-only projects are eliminated. During estimation this traversal finds no relations beneath GroupPlan leaves. In the concrete plan, bound slot qualifiers are internal.db.a/internal.db.b, but the lookup map only indexes bare aliases and relation.getQualifier() (catalog/database), so those bindings are empty there too. Both joins consequently get the same expansion fingerprint despite different tables and fan-outs; an expansion pinned for the first is applied to the second without a freshness guard. Resolve stable table identities through memo groups and index the full bound qualifiers consistently with EXPLAIN.
There was a problem hiding this comment.
Implemented in the new commit of this branch (hbo-structinfo-squashed):
- An expansion key now requires every operand of the join conditions — the equality pairs and the residual conditions — to be a direct slot of a catalog table, i.e.
SlotReference.getOriginalTable()andgetOriginalColumn()present. The operand identity isqualifiedNameOf(originalTable) + "." + originalColumn.getName(), read from the slot itself, so it is identical while the group is estimated (GroupPlanchildren) and while the chosen plan is printed, and it does not depend on the alias or on the qualifier shape (internal.db.a). - The key is the structural hash of: the join type, the sorted equality pairs rendered with those operand identities (
EqualTo(col(internal.db.t1.k),col(internal.db.t2.k))), the sorted residuals (literals kept) and the sortedoperands:[...]. Duplicate alias bindings, subtree traversal and relation lookups are gone. - A join whose operand is not a table column (cast, aggregate output, derived alias) has no key:
canonicalOf/fingerprintOfreturn empty, so EXPLAIN prints nojoin type=join_expansionline and no lookup can match it. That is the fail-closed behaviour you asked for instead of a colliding key. - An outer join additionally names the columns of each side (
sides:[...], read from the child output slots), soA LEFT JOIN BandB LEFT JOIN Astay distinct while remaining stable across the two call sites. Inner joins stay commutative. - Tests:
HboJoinConditionsTestnow pinst1 AS a JOIN t2 AS bvst3 AS a JOIN t4 AS b(different keys), a computed operand (no key), the outer-join side, the residual literals and that the key is not the sha256 of the canonical text. The expansion regression suite reads its fingerprint/struct from the EXPLAIN annotation and has not been re-run on a FE rebuilt from this commit yet; I will report the result once it is.
| // identity names the sub tree only, not how often the plan references it | ||
| out.count(child.mapping.size()); | ||
| for (Expression mapped : child.mapping) { | ||
| out.text(renderExpression(mapped, mode)); |
There was a problem hiding this comment.
[P2] Keep value-defining literals in set-operation mappings
These mappings define branch output values, so folding their literals can change distinct cardinality. For SELECT a AS x FROM t UNION SELECT a % N AS x FROM u, BuildAggForUnion produces Aggregate(gb=x) -> UnionAll(Project(t.a), Project(u.a % N)). With t={0} and u=0..999, N=2 yields two groups and N=1000 yields 1000, but NO_LITERAL encodes both mappings as Mod(col(u.a),lit(*)). The aggregate's learned key therefore collides, and unchanged scans with no predicates pass both matching and freshness. Preserve mapping literals in both modes, as grouping definitions already do; the existing computed-branch golden pair currently records this collision.
There was a problem hiding this comment.
不需要. NO_Literal 模式本身就意味着常熟部分的差异会带来行数误差, 注册NO_LITERAL hbo 就意味着用户认为可以忽略这个误差
| String table = HboScanDescriptor.qualifiedNameOf(scan.getTable()); | ||
| // the input rows of a scan node are the rows it read, i.e. the number a catalog row | ||
| // count (which also ignores the predicates) can be compared with | ||
| recordedRowsByTable.merge(table, input.getInputRows(), Long::sum); |
There was a problem hiding this comment.
[P2] Compare learned inputs against the same row population
Filter(k=1) -> Scan(t) on 100000 sorted distinct keys can physically read one row: storage key ranges prune the row bitmap before raw_rows_read/ScanRows are incremented. RuntimeProfile now records that counter, but the live descriptor contains all 100000 selected-partition rows. An immediate identical query therefore compares 1 with 100000, calls its fresh feedback stale, and cannot reuse the measured filter or parent join/aggregate count. Zero reads have the opposite failure: recorded == 0 is still skipped as unmeasured, so an absent-key result survives insertion of matching rows. Snapshot a comparable input-population baseline and distinguish missing measurements from measured zero.
There was a problem hiding this comment.
learned HOB 是老版本的功能, 且老版本里已经是不可用状态. 不在这个pr里处理
| continue; | ||
| } | ||
| compared = true; | ||
| String tableState = recorded == live[0] ? STATE_LIVE |
There was a problem hiding this comment.
[P2] Enforce strict version matching for learned entries
With hbo_row_count_change_ratio=0, this branch still declares equal input counts live without checking a recorded visible version. For Aggregate(gb=t.v) -> Scan(t), learn one group from 1000 identical values, then update those values to many distinct values. After compaction and row-count refresh the population can again be 1000 while the visible version has advanced. The new version-free key and unchanged scan predicates match, so applyLearnedStats applies the obsolete one-group estimate despite strict mode. ScanPlanStatistics retains a scan reference but no immutable version snapshot. Record the learned input versions and require equality in nonpositive-tolerance mode, as the pinned path does.
There was a problem hiding this comment.
learned hbo 的问题是老版本的遗留问题, 不在这个pr处理
| Optional<PlanNodeAndHash> planNodeAndHashOpt; | ||
| // the publish path keys a join with the constant agnostic form, and that is the | ||
| // form which the read side publishes for skew ratios as well | ||
| planNodeAndHashOpt = HboUtils.getHboPlanNodeAndHash(physicalHashJoin, |
There was a problem hiding this comment.
[P2] Check learned freshness before applying the skew discount
This parallel consumer now uses a version-free key but calls getMatchedPlanStatistics, which checks predicates/runtime-filter safety without learnedEntryFreshness. After a skewed join is learned, substantial uniformly distributed growth can exceed the input-row tolerance and eliminate that skew. HboStatsCalculator.applyLearnedStats rejects that run as stale, yet broadcast costing still reads its old skew ratio and multiplies probe/build work by 0.1, potentially choosing an inappropriate broadcast plan. Previously the embedded visible version prevented this hit. Retrieve the matched entry and validate its input freshness before using its skew measurements.
There was a problem hiding this comment.
learned hbo 的问题是老版本的遗留问题, 不在这个pr处理
| // wildcard that matches by fingerprint alone. A published entry always carries the rows of at | ||
| // least one scan (see InputTableStatisticsInfo#hasInputTables, the publish path drops entries | ||
| // which recorded none), so an empty input list can only be a manual injection | ||
| for (RecentRunsPlanStatisticsEntry injected : planStatistics.getRecentRunsStatistics()) { |
There was a problem hiding this comment.
[P2] Let actual feedback replace an injected learned placeholder
After HBO SET LEARNED STATISTICS for a join's NO_LITERAL key, the cache contains an empty-input wildcard. The first actual profile publication does not replace it: Profile.updatePlanStatistics cannot FULL_MATCH its zero inputs with the real nonempty input list and appends the measurement. Later identical executions replace only that measurement, leaving [wildcard, measured] below the default ten-entry limit indefinitely. This loop always returns the wildcard, so its arbitrary row count keeps overriding real feedback and bypassing drift checks. Remove the placeholder when actual same-key feedback arrives, or prefer a matching measured entry once one exists; otherwise LEARNED behaves like a persistent pinned override.
There was a problem hiding this comment.
手动注入learned条目的场景是测试场景,生产环境不会用手动注入learned 条目
| break; | ||
| case JOIN: | ||
| out.text(node.flattenable ? "inner" : String.valueOf(node.joinType)); | ||
| out.text(distinctIdentityConditions(node.conditions)); |
There was a problem hiding this comment.
[P2] Distinguish marking joins from filtering semi joins
Project(t.k, mark AS m) -> LEFT_SEMI_JOIN(mark=true, t.k=u.k) -> [Scan(t), Scan(u)] preserves every t row, while the same semi join without the marker filters unmatched rows. For nonnullable keys, projected t.k IN (SELECT k FROM u) produces the first plan; an ordinary semi join produces the second. With t={1,2}, u={1}, they output two versus one rows, but this key writes only join type and merged conditions, omitting isMarkJoin(). Both literal modes collide, and computeJoin can apply the other plan's learned/pinned count because their input scans are identical. Encode marking semantics in the join identity and cover both plans.
There was a problem hiding this comment.
GroupStructInfo 增加属性记录join是否是mark
There was a problem hiding this comment.
Fixed in the branch ([fix](fe) Include the mark-join flag in the join identity):
GroupStructInfo.Nodenow carriesmarkJoin(fromJoin.isMarkJoin()), the canonical writes it asJ{<type>,mark,c:[...]}and the structural key writes it as a field, soProject(mark AS m) -> LEFT_SEMI_JOIN(mark=true, ...)and the plainLEFT_SEMI_JOINover the same scans no longer share a key in either literal mode.- Two corpus cases pin the pair (
join_mark_semi/join_plain_semi) and a test asserts the keys differ in both modes and that the canonical writes the flag. - Note: because the flag is a field of the join identity, the fingerprints of all join cases moved once (the regenerated corpus shows that change explicitly).
- Tests:
org.apache.doris.nereids.stats.*+ the HBO command suite, 200 cases pass. The regression suites were not re-run against a FE rebuilt from this commit.
FE UT Coverage ReportIncrement line coverage |
TPC-H: Total hot run time: 28773 ms |
TPC-DS: Total hot run time: 151139 ms |
ClickBench: Total hot run time: 24.31 s |
FE Regression Coverage ReportIncrement line coverage |
### What problem does this solve?
Problem Summary: the key of an injected join expansion named the alias a query gave a relation
(`a.k = b.k`), so `t1 AS a JOIN t2 AS b` and `t3 AS a JOIN t4 AS b` shared one key although they read
other tables and expand differently; the attempt to add the alias to table bindings could not work,
because a group being estimated has `GroupPlan` children (no relation below them to inspect) and the
concrete plan qualifies the operands as `internal.db.a`, which the alias table did not index. The
expansion entry carries no data state to guard it, so the factor of one join was applied to the other.
An expansion key now requires every operand of the join conditions to be a direct slot of a catalog
table (`SlotReference.getOriginalTable()` and `getOriginalColumn()` present): the key names the column's
own table and name, so it is the same while a group is estimated and while the chosen plan is printed,
and it does not depend on aliases or on the shape of a qualifier. A join whose operands are computed
columns (a cast, an aggregate output, a derived alias) has no expansion key at all: EXPLAIN prints no
`join type=join_expansion` line for it and no lookup matches it, instead of sharing a key with another
table pair. An outer join also keeps the columns of each side, which is what distinguishes the
preserved side; an inner join stays commutative.
### Release note
A manually pinned join expansion entry is now bound to the tables its conditions read; a join whose
operands are not direct table columns can no longer carry an expansion entry.
### Check List (For Author)
- Test: Unit Test
- `HboJoinConditionsTest` pins the outer join side, the residual literals, the two joins over
different tables with the same aliases, the computed operand without a key and the structural
hash (5 cases pass); the regression suite which injects expansions reads its fingerprint and
struct from the EXPLAIN annotation, and has not been re-run against a FE rebuilt from this commit.
- Behavior changed: Yes - expansion keys are bound to the tables of the conditions; computed operands
are refused.
- Does this need documentation: No
### What problem does this solve?
Problem Summary: the identity of a join was its type and its conditions. A mark join, which implements a
non-nullable `IN` subquery as `Project(mark AS m) -> LEFT_SEMI_JOIN(mark=true, t.k = u.k)`, preserves
every row of its left side and produces a marker column, while the same semi join without the marker
drops the unmatched rows: with t = {1, 2} and u = {1} the first plan outputs two rows and the second
one, with the same join type, the same condition and the same input scans. Both plans rendered one key,
and since their inputs are identical the freshness comparison could not separate them either, so a
learned or pinned count measured on one was applied to the other.
The mark flag is now part of the join identity in both forms: the canonical string writes `J{<type>,mark,
c:[...]}` and the structural key writes the flag, so the two plans keep different keys in both literal
modes. Every join fingerprint moved once by this change (the flag is a field of the identity even when it
is false).
### Release note
A mark join and the plain semi join of the same columns no longer share an hbo statistics entry.
### Check List (For Author)
- Test: Unit Test
- Two corpus cases pin the mark semi join and the plain one over the same scans; a test asserts their
keys differ in both literal modes and that the canonical writes the flag. The regenerated file
gained those cases and moved the fingerprints of the join cases (the intended change), and 200
cases of the stats package plus the HBO command suite pass. The regression suites were not re-run
against a FE rebuilt from this commit.
- Behavior changed: Yes - the mark-join flag is part of a join key.
- Does this need documentation: No
|
run buildall |
|
/review |
There was a problem hiding this comment.
Reviewed all 116 changed paths at head 5a2df43e76166bd3fed18aed527a1b748fcb3594 using the supplied aggregate diff. Five new findings: four P2 issues affecting outer-join identity, null-extension, learned-feedback association and persisted replacement revisions; one P3 issue affecting live cache configuration. These can misapply optimizer estimates or lose persisted statistics; no transaction-data visibility defect was established.
Critical checkpoint conclusions:
- Goal and proof: the structural-key, injection, feedback and persistence pipeline is present, but the inline counterexamples show it is not fully correct. Supplied tests were inspected statically.
- Scope and reuse: the broad planner/profile/command changes serve this feature and reuse existing providers/internal SQL; no unrelated source change identified.
- Concurrency and locking: FE-local SET/DELETE/load serialize consistently and internal planner reads avoid lock reentry. Cross-FE timestamp generations fail to protect a replacement (P2 inline).
- Lifecycle: daemon retry/enable/stop behavior, memo/cache ownership and postprocessor rebuild order checked. Parent/fragment/runtime-filter registration follows rebuilding; no separate lifecycle defect confirmed.
- Configuration: mutable ratios and persistence flag are read at use; the pinned bound is explicitly startup-only. The new relation-table cache misses the mutable size/expiry callback (P3 inline).
- Compatibility: structural keys replace legacy fingerprints; internal-table column checks reject an incompatible schema. Mixed-version/restart behavior was not executed.
- Parallel paths: logical/physical plans, both literal modes, cloud/non-cloud access, partial/storage aggregation exclusions and the separate skew consumer checked. Outer self-join and nullable-side definition paths have the inline identity defects.
- Conditional/error paths: type/scope/value/STRUCT/age validation, unavailable internal storage and documented unknown-baseline behavior checked. No additional unsupported-shape or error-propagation issue confirmed.
- Test coverage: focused unit, golden and positive/negative regression coverage is substantial; the five reported cases and true distributed replacement races lack adequate coverage. No builds or tests were run under the static-only review contract.
- Expected results: persistence output and generated golden expectations were checked against code. The captured cluster corpus is historical syntax/shape evidence, not execution against this head.
- Observability: EXPLAIN/SHOW expose keys, modes, baseline/guard/application states; load/persistence/snapshot failures include useful logging. No additional critical metrics gap identified.
- Persistence and failover: FE-local authority, one-time loading and best-effort SQL are explicit. They do not protect a newer cross-FE persisted row from timestamp-bounded deletion (P2 inline); restart/failover was not runtime-validated.
- Writes and atomicity: no transaction-row, MoW or version-visibility mechanism changed. Local cache/write ordering is serialized; the persisted-statistics replacement race remains.
- FE/BE variables: no new transmitted fields or function ABI requiring corresponding BE changes identified.
- Performance and memory: scan caps, cached descriptions, empty-cache/relation guards and administrative cache bounds checked. Retained relation-key supersets were treated as a documented optimization tradeoff without a substantiated material impact. No runtime performance claim is made.
- Other concerns/user focus: no additional user focus was supplied; all initial risks and candidates have explicit evidence-based dispositions in the review ledger.
Existing review context: the mark-join flag and different-table inner-expansion bindings are fixed in this head. Six existing P2 concerns about expression metadata, set mappings, learned freshness/populations, skew freshness and learned-wildcard precedence remain known context and were not reposted. No existing P0/P1 comment requires an ID in existing_blocking_comment_ids.
Coverage limits: static analysis only; no builds, tests, cluster runs or performance measurements. All changed paths and assigned risk questions were covered, with no unresolved review candidate remaining at delivery.
Convergence: true after 3 rounds. All three final-round reviewers returned NO_NEW_VALUABLE_FINDINGS on the identical five-comment set; required coverage and candidate/risk dispositions are complete.
Review converged after 3 round(s).
| if (!slot.getOriginalTable().isPresent() || !slot.getOriginalColumn().isPresent()) { | ||
| return Optional.empty(); | ||
| } | ||
| return Optional.of(HboScanDescriptor.qualifiedNameOf(slot.getOriginalTable().get()) + "." |
There was a problem hiding this comment.
[P2] Keep each equality operand bound to its outer-join input
LEFT_OUTER_JOIN(a.k=b.v, Scan(t a), Scan(t b)) and the same join on a.v=b.k, selecting all four columns, get identical expansion keys here. For t(k,v)={(1,1),(2,1)}, they produce three versus two rows. Both operands become the original-table names t.k/t.v, the equality sorts them, and sideOperands sees the same {t.k,t.v} on both sides. A factor of 1.5 injected for the first therefore estimates three rows for the second as well, with no binding/freshness guard. Preserve the input/occurrence of each operand before canonicalizing an outer self-join, and cover this reversed-column pair. This is distinct from the existing different-table INNER JOIN thread: both inputs here read the same table.
There was a problem hiding this comment.
Fixed in the branch ([fix](fe) Order the operands of an outer join expansion by their side):
- The operands of an expansion pair are now ordered by the side they are read from: the operand which belongs to
join.child(0)is written first (the side is resolved by the slot exprId against the child outputs, so it is the same while a group is estimated withGroupPlanchildren and while the chosen plan is printed), and the pair is not sorted for a join whose sides are not interchangeable. - Consequences:
t a LEFT OUTER JOIN t b ON a.k = b.vand the same join ona.v = b.know rendert.k = t.vandt.v = t.k(different keys), while the same pair written in either order (b.v = a.kagainsta.k = b.v) normalizes to one key. - An inner join keeps its operands sorted: the optimizer may swap its children and the count of an inner self join does not depend on which occurrence is called the left one.
- Tests:
HboJoinConditionsTestpins the reversed outer self join, the either-order pair and the inner self join (6 cases), andorg.apache.doris.nereids.stats.*+ the HBO command suite pass (201 cases). The expansion regression suite has not been re-run against a FE rebuilt from this commit.
There was a problem hiding this comment.
对不能交换顺序的join, join等式的左右需要和join孩子的左右保持一致
| } | ||
| Map<ExprId, Expression> definitions = new LinkedHashMap<>(); | ||
| for (Child child : node.children) { | ||
| definitions.putAll(child.node.columnDefinitions); |
There was a problem hiding this comment.
[P2] Preserve null-extension in inherited column definitions
Consider Aggregate(gb=x) -> LEFT_OUTER_JOIN(t.k=u.k) -> [Scan(t), Project(u.k,coalesce(u.v,0) AS x)->Scan(u)], versus computing that same x in a project above the join. With t.k={1,2} and u={(k=1,v=0)}, the first has groups {0,NULL} (two rows); the second has {0} (one row). This merge forwards the right-side x definition unchanged across null-extension, so applyColumnDefinitions rewrites both grouping keys to the same Coalesce expression. Transparent projects then give both plans identical keys in both literal modes, and identical scan baselines pass freshness, allowing the other plan's pinned/learned group count to apply. Retain the nullable-side evaluation boundary when propagating definitions (or reject unsupported definitions), and add the paired plans.
There was a problem hiding this comment.
null-extend 标签, 同时在null-extend 节点上添加编号
| // rows, and the drift comparison of the published entry expects the rows of every scan | ||
| PlanStatisticsWithInputInfo published = outputPlanStatisticsMap.get(publishedKey); | ||
| outputPlanStatisticsMap.put(publishedKey, new PlanStatisticsWithInputInfo( | ||
| published.getId(), published.getPlanStatistics(), |
There was a problem hiding this comment.
[P2] Keep each learned output count with the inputs that produced it
UnionAll(Filter(k<10, Scan(t)), Filter(k<1000, Scan(t))) over the same unaliased key table containing k=0..999 can publish two scans under the same S fingerprint. The filters are embedded into their scan operators. If the narrow scan is visited first, this retains its output=10, while InputTableStatisticsInfo.merge chooses the other scan's larger input=1000 together with its predicate and RelationId. On the next identical query, computeFilter's scan-key fallback matches that retained k<1000 predicate and freshness accepts 1000 against the table's 1000 rows, then applies the unrelated output=10 to the 1000-row branch. Keep output and corresponding inputs together, using separate recent-run entries for different predicate/input signatures. Unlike the existing ScanRows-population thread, this mismatched pair passes both matching and freshness.
There was a problem hiding this comment.
learned 条目不在本pr处理. learned hbo 要删掉重新实现
| String sql = "DELETE FROM " + FULL_QUALIFIED + " WHERE `fingerprint` = '" | ||
| + StatisticsUtil.escapeSQL(fingerprint) + "' AND `" + CREATE_TIME_COLUMN | ||
| + "` <= '" + createTimeLiteral(generation) + "'"; | ||
| StatisticsUtil.execUpdate(sql); |
There was a problem hiding this comment.
[P2] Use a write revision that distinguishes replacements across FEs
create_time comes from System.currentTimeMillis() before the FE-local lock, so it is not a unique generation across FEs. Two concurrent SETs for one fingerprint can capture the same millisecond; after B's INSERT replaces A's row, a conditional stale deletion on A still matches B's acknowledged replacement through this <= predicate. Clock skew can likewise give a later replacement a smaller timestamp, and tombstoneCovers will suppress that replacement during a pending load too. This violates the stated guarantee that a newer SET on another FE survives removal of the judged entry, even when every SQL operation succeeds. Persist a distinct revision/CAS token for the entry and target that revision when a known entry is removed, rather than ordering independent writes by local wall-clock time.
There was a problem hiding this comment.
修改del的顺序, 先删除table中的行, 成功再删除cache中的行. 去掉tombstoneCovers 机制
| Cache<String, Map<RelationId, Set<Expression>>> scanToFilterCache = | ||
| buildHboScanToFilterCache(cacheNum, expireSeconds); | ||
| scanToFilterCache.putAll(provider.scanToFilterCache.asMap()); | ||
| provider.scanToFilterCache = scanToFilterCache; |
There was a problem hiding this comment.
[P3] Rebuild the relation-table cache when its configuration changes
relationTableIdCache is constructed with hbo_plan_info_cache_num and expire_hbo_plan_info_cache_in_fe_second, but this callback never rebuilds it while updating every sibling cache. For example, starting with both limits disabled and then setting positive bounds through ADMIN leaves this new cache unbounded and non-expiring for subsequent filtered queries until FE restart; reducing a positive size/TTL likewise keeps the original limits. Both settings are mutable and invoke this callback. Rebuild the relation-to-table cache with the updated size/expiry and carry over its entries, as done for the adjacent scan-filter cache.
There was a problem hiding this comment.
这是learned hbo 专属的成员. learned hbo 后续pr要完全重做, 采用pinned 的处理方式替代 relatoinId的作用. 不在这个pr里实现
TPC-H: Total hot run time: 28272 ms |
TPC-DS: Total hot run time: 150859 ms |
ClickBench: Total hot run time: 24.23 s |
FE UT Coverage ReportIncrement line coverage |
FE Regression Coverage ReportIncrement line coverage |
### What problem does this solve?
Problem Summary: the operand identity of a join expansion is the column of a table, so in a self join
both sides of an equality resolve to the same table: `t a LEFT OUTER JOIN t b ON a.k = b.v` and the same
join on `a.v = b.k` both rendered `t.k = t.v` (the pair was sorted for commutativity), and the side
information (`sides:[...]`) is the same for both sides of a self join. The two plans keep a different
number of rows, and an expansion factor pinned for one was applied to the other without any data state
to guard it.
The operands of a pair are now ordered by the side they are read from: the operand of the left child is
written first, so `a.k = b.v` and `a.v = b.k` keep different keys, while the same pair written in either
order (`b.v = a.k` against `a.k = b.v`) keeps one. An inner join stays commutative: the optimizer may
swap its children and its count does not depend on which occurrence is called the left one, so its
operands remain sorted by their identity.
### Release note
An hbo join expansion entry of an outer self join is no longer applied to the join of the same table with
the columns reversed.
### Check List (For Author)
- Test: Unit Test
- `HboJoinConditionsTest` pins the outer self join with the columns reversed (different keys), the
same pair written in either order (one key) and the inner self join (one key); 201 cases of the
stats package plus the HBO command suite pass.
- Behavior changed: Yes - the operand order of an outer join expansion pair is the side it is read from.
- Does this need documentation: No
There was a problem hiding this comment.
Static review of PR #69322 at e3ddd11. Four new P2 findings are recorded inline: an ignored join-expansion factor with unknown column statistics, an obsolete regression assertion, ambiguous null-extension paths, and a separate MAP-literal identity collision. No new P0/P1 finding was established, and the supplied threads contain no P0/P1 finding to carry forward. Existing issues were treated as duplicate fences.
Critical checkpoint conclusions:
- Goal and proof: the change connects structural HBO keys, manual statistics management, learned feedback and diagnostics. Static tracing establishes the four defects; inspected tests do not establish executed success.
- Scope and reuse: the broad change is focused on HBO and shares descriptor, identity, storage and guard helpers. No separate scope defect was substantiated.
- Concurrency and locks: planner-local group state is single-threaded; admin and loader writes serialize under pinnedLoadLock while planning reads remain lock-free. Internal SQL can delay other admin operations. The existing cross-FE replacement/deletion race remains known context.
- Lifecycle: one loader per FE retries until its one-time load succeeds; persistence-off leaves it available for a later enable. Memo descriptions and planning-time fingerprint snapshots have the expected query/cache lifetimes. No C++ static-initialization change applies.
- Configuration: freshness/filter ratios and persistence are read dynamically; pinned-cache size is startup-only, and the scan limit is a session setting. The already-reported relation-table cache callback omission is not reposted.
- Compatibility: lookup and publication consistently use the new FE keys. Internal-table missing-column detection rejects incompatible schemas with operator guidance. No new thrift field or BE row-storage format is introduced; rolling upgrades were not executed.
- Parallel paths: filter/join/aggregate, logical/physical scans, staged aggregates, join types, broadcast costing and post-process group propagation were inspected. The unknown-statistics join branch misses the new multiplier.
- Conditional checks: freshness, FILTER_SMALL, eligibility, scan limits and outer-join bounds have concrete purposes. The null-boundary path representation fails when a child index has multiple digits.
- Test coverage: unit, golden and regression coverage exercises ordinary identity, injection, drift, persistence and negative input cases. Multi-digit paths and MAP literal collisions lack paired cases; restart, multi-FE and persistence-fault execution are not covered by this review.
- Expected results: the new persistence output is consistent with its SQL under static inspection. The expansion regression has a deterministic assertion incompatible with its new equality-only key.
- Observability: EXPLAIN/SHOW expose keys, baselines and application state; load/write failures have logs. The ignored-factor path nevertheless reports the expansion as applied. The existing swallowed stale-delete error remains known context.
- Persistence and failover: internal SQL supplies ordinary transaction/journal behavior, with no custom EditLog record. Same-FE load/SET/DELETE ordering is serialized; the known cross-FE delete/replacement concern is not reposted. No failover simulation was performed.
- Writes and atomicity: local SET/DELETE changes the cache after successful persistent SQL and retains it on explicit failure. No user-table write or delete-bitmap path changes here.
- FE/BE propagation: no new wire variable is added; the scan limit uses FE forwarding metadata and EXPLAIN display stays local. Existing UDF metadata accessors cover all six Java/Python wrappers.
- Performance and memory: empty-cache guards, relation prefilters, memoized descriptions and scan-count limits avoid unnecessary work. No additional concrete performance defect was established; allocation/runtime costs were not measured.
- Other correctness: the two new identity collisions can apply unrelated pinned counts. Existing cast/order-field, ARRAY serialization, aggregate-producer, null-marker and learned freshness/matching/skew issues remain duplicate context. UDAF DISTINCT and direct VARIANT expansion hypotheses were dismissed after upstream legality/binding checks.
There was no additional user-provided focus. All changed paths and tracked candidates/risk items have dispositions. This review is static only: no builds, tests, cluster restart or failover execution was performed.
Review converged after 3 round(s).
| innerJoinStats = estimateInnerJoinWithEqualPredicate(leftStats, rightStats, join); | ||
| } | ||
|
|
||
| if (expansion != 1.0) { |
There was a problem hiding this comment.
[P2] Apply the expansion to the unknown-statistics fallback
For INNER(t.k=u.k, Scan(t,1000 rows), Scan(u,1000 rows)) with unknown equality-column statistics, estimateInnerJoin returns at lines 265-273 before reaching this multiplier. This occurs with missing column statistics, or explicitly with enable_stats=false while HBO remains enabled. Injecting a factor of 100 therefore still estimates 1000 rows instead of 100000, although the Cartesian cap is 1000000; applyPinnedJoinExpansion also reports the factor as applied. Route the fallback estimate through the expansion tail and cover unknown-column inputs as well as analyzed tables.
| assertTrue(literalMatcher.find(), "no condition fingerprint annotation:\n" + annotation(literalQuery)) | ||
| def literalFingerprint = literalMatcher.group(1) | ||
| def literalCanonical = literalMatcher.group(2) | ||
| assertTrue(literalCanonical.contains("lit(1:"), literalCanonical) |
There was a problem hiding this comment.
[P2] Update the regression for equality-only expansion keys
The query above has the equality t1.a=t2.a and residual t1.b>t2.c+1, but HboJoinConditions.keyParts now visits only hash conjuncts. Its canonical cannot contain lit(1:), so this assertion fails whenever the preceding setup succeeds, before the final SET/SHOW checks. testAResidualDoesNotChangeTheExpansionKey already asserts the new contract. Replace this expectation with equality-key equivalence across residual changes and verify that residual selectivity is applied after the expansion.
| } | ||
|
|
||
| private NullExtended prepend(int index) { | ||
| return new NullExtended(index + path, child); |
There was a problem hiding this comment.
[P2] Delimit child indexes in null-extension boundary paths
Concatenating decimal indexes conflates [1,0] with [10]. A reduced common input is:
Agg(group by x OR group by y)
CROSS(F(f), LEFT(LEFT(a,P(u,x)),p), s0,...,s7,
TopN(LEFT(b,P(u,y))))
Here P(u,z) projects u.k, coalesce(u.v,0) AS z; each LEFT matches keys, and the eight s inputs are scans. The sorted leaf prefixes F,J,S,...,S,T put the two branches at indexes 1 and 10. x gets path 0 through the enclosing LEFT's left child, then 10 at CROSS; y keeps its empty path through TopN, then also gets 10. Both grouping expressions become NullExtended<10>(Coalesce(col(u.v),lit(0:INT))). With a.k={1,2}, b.k={2}, u={(1,0)} and singleton dummy inputs, x has groups {0,NULL} and y only {NULL}. The common child and scan baselines are identical, so both literal modes collide and a pin can apply the other count. This uses 14 scans, below the default limit. Encode paths as delimited integers or a structured list. This is distinct from cache-order instability: the paths are stable but ambiguous.
| } | ||
| return text.append(']').toString(); | ||
| } | ||
| return String.valueOf(literal.getValue()); |
There was a problem hiding this comment.
[P2] Encode MAP literal keys and values separately
MapLiteral reaches this fallback, whose getValue() is a map of literals. StringLikeLiteral.toString() quotes values without escaping: the one-entry MAP<STRING,STRING> values (key=a'='b, value=c) and (key=a, value=b'='c) both serialize as {'a'='b'='c'}. Encoding that complete text afterward preserves the collision. Consequently Filter(map_contains_key(mapLiteral,t.k))->Scan(t) has the same WITH_LITERAL fingerprint for both maps, while STRING k='a' matches only the second; identical scans pass freshness and an exact pin applies the unrelated count. Typed map literals are reachable through visitMapLiteral and a constant cast to MAP<STRING,STRING>. Recursively encode each key/value and its boundaries. This is separate from the existing ARRAY-element thread: maps never enter that special branch, so fixing its loop leaves this fallback collision.
…ingerprint preimage
### What problem does this solve?
Problem Summary: the structural key of a group wrote the rendered text of its conditions, its branch
mappings and its header expressions as one length prefixed string. Within that string the values of an
expression were still concatenated by the printed grammar (`lit(` ... `,` ... `:type`), so a value which
carried those characters could stand for a field boundary of its own: the array `['a','b']` and the
singleton array whose element is `a:TEXT),lit(b` produced the same preimage, although they match
different rows.
The fingerprint preimage now serializes expressions structurally as well: every expression writes its
kind, its value defining fields (a literal's type and value, a slot's qualifier and name) and its
children, each with its own length, and the conditions are sorted and de-duplicated over those bytes.
A composite literal keeps one field per element, so an element cannot forge the boundary of another.
The display form (canonical, shape, EXPLAIN, the struct a user pastes) is unchanged; only the fingerprint
moves. A missing semantic field (a cast target, an order direction) is not addressed here and remains a
per-expression follow-up.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- The regenerated corpus changes only its fingerprint lines (the shapes, canonicals and scan tokens
are byte identical); 200 cases of the stats package plus the HBO command suite pass.
- Behavior changed: Yes - all fingerprints moved once (the display form did not).
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of #69322 at 6a5632d. Four distinct new findings: three P2 issues in mapping identity, residual expansion and pending-load visibility, plus one P3 metadata-lifetime issue. The feature is wired end to end, but these defects need correction.
Existing inline threads were treated as duplicate fences. Still-applicable concerns include ARRAY/MAP literal encoding, cast/order metadata, aggregate producer identity, ambiguous null-boundary paths, learned input/freshness/skew handling, unknown-statistics expansion, the stale residual regression assertion, cross-FE deletion and stale-cleanup error reporting. Mark-join identity, outer/equality side orientation and the former cache-order boundary-ID issue have targeted fixes. No existing P0/P1 inline finding was present to carry forward. No additional user-provided focus was supplied.
Critical checkpoint conclusions:
- Goal and proof: structural identities, pinned management, learned feedback and diagnostics are present; the new findings and known threads prevent a clean correctness assessment. Tests exist but do not cover the four new triggers.
- Scope and clarity: changes are cohesive FE HBO work; independently maintained expression encoders diverge in the mapping finding.
- Concurrency and locks: optimization is single-threaded; query snapshots use concurrent caches and RuntimeProfile child traversal is locked. Commands and loader share an FE-local monitor, with SQL under it; no inverse lock cycle was demonstrated. That lock does not guarantee backend publication.
- Lifecycle: memo snapshots, partial-stage exclusions and one-shot loader ownership were traced. Pending-load resurrection and historical relation-key retention are defects; no C++ initialization change applies.
- Configuration: filter/drift ratios and persistence toggle are read on use; max-scans is session-scoped/forwarded; pinned capacity is startup-only. The already-reported mutable learned-cache callback omission remains.
- Compatibility: no new FE/BE wire symbol or plan format; all six UDF implementations supply the added API. Structural keys replace old learned identities. Internal-table schema checks fail explicitly on incompatible columns; mixed-version/restart behavior was inspected statically.
- Parallel paths: logical/physical plans, exact/no-literal lookup, partial/storage aggregations, legacy/MOW deletion and the separate cost consumer were checked. The mapping encoder is a missed path; cost freshness remains an existing thread.
- Conditional guards: scan limits, unsupported shapes, small-filter gating and freshness fallbacks were traced. The new identity and residual findings use supported, known-stat paths.
- Test coverage: unit/golden/regression coverage includes ordinary injection, validation, growth, pruning, caps, identity and counters; mapped DISTINCT, NULL-sensitive residuals, publication/load races and index retirement need coverage. No builds or tests were run, as required.
- Changed results: the persistence .out matches its suite's static SELECT shapes/values; the known residual-key assertion contradicts the new equality-only contract. Golden fixtures were inspected with their generators; generation provenance was not independently established.
- Observability: EXPLAIN use/skip/expansion annotations, SHOW baselines and SQL/load diagnostics support investigation. Stale-cleanup client error suppression is already reported.
- Persistence/EditLog/failover: manual entries use internal-table SQL and existing transactions, with no new FE edit-log payload. COMMITTED and VISIBLE are distinct; the pending-load finding violates the required deletion lifecycle.
- Data writes/atomicity: user-table read/write, partition versions and delete-bitmap algorithms are unchanged. Internal metadata deletion/load can restore a removed override; cross-FE replacement deletion is an existing issue. Crash behavior was not executed.
- FE/BE variables: existing profile input_rows is reused; no new wire field. FE-only max-scans forwarding is present.
- Performance and memory: cache-empty/relation/scan-count gates and memoized descriptions reduce hot-path work; the unbounded historical relation index defeats the pinned metadata bound.
- Other concerns and limits: all changed-file families and risk items received a static disposition; no additional focus or unresolved candidate remains. Runtime execution, builds, failover experiments and result-generation provenance were outside the authorized static review.
Review converged after 2 round(s).
| private static void appendExpressionKey(ShapeKeyWriter out, Expression expression, LiteralMode mode) { | ||
| String name = expression.getClass().getSimpleName(); | ||
| out.text(name.isEmpty() ? expression.getClass().getName() : name); | ||
| List<Expression> children = expression.children(); |
There was a problem hiding this comment.
[P2] Preserve DISTINCT in set-operation mapping keys
With agg_phase=3, compare SELECT COUNT(v) AS x FROM t GROUP BY k UNION SELECT x FROM u with COUNT(DISTINCT v) in its first branch (u.x is BIGINT). BuildAggForUnion produces the reduced tree:
Aggregate(gb=x)
UnionAll
Aggregate(gb=k, Count(v) AS x) -> Scan(t)
Scan(u), mapping u.x
The DISTINCT variant keeps this logical shape, but this encoder writes only Count and its child, omitting isDistinct(). The child aggregate key contains only k, so both actual fingerprints collide in both modes. For t={(1,1),(1,1)} and u.x={1}, the UNION has two versus one rows; identical scan baselines accept the other query's pinned count. Encode the semantic fields already handled by expressionName in this binary path and cover mapped DISTINCT aggregates. Unlike the existing cast/order text-normalization and aggregate-producer threads, the canonical text already distinguishes this pair; the separate mapping hash loses that distinction.
| if (expansion != 1.0) { | ||
| // the factor is the fan out of the equality conditions: scale their estimate before the | ||
| // residual conditions (which keep their own selectivity) are applied | ||
| innerJoinStats = new StatisticsBuilder(innerJoinStats) |
There was a problem hiding this comment.
[P2] Preserve residual selectivity when expanding equality rows
For the reduced plan INNER(hash=a.k=b.k, other=(a.v IS NULL OR b.v IS NULL), Scan(a), Scan(b)), let each analyzed input have 1000 rows, k NDV=1000/no NULLs and v NDV=500/NULLs=500. The cross-input OR remains a join residual. A factor of 100 changes the equality estimate from 1000 to 100000 here, but StatisticsBuilder retains each v's absolute null count and base-column identity. FilterEstimation.visitIsNull therefore still estimates 500 for each disjunct, and visitOr returns at most 1000, effectively discarding the registered expansion. Later normalization cannot restore the lost factor. Scale the dependent populations consistently before applying residuals, or preserve the unexpanded residual selectivity. This reaches the multiplier with known column statistics and is separate from the existing unknown-statistics early-return issue.
| // this returns true, and reports the failure to the user otherwise | ||
| String sql = "DELETE FROM " + FULL_QUALIFIED + " WHERE `fingerprint` = '" | ||
| + StatisticsUtil.escapeSQL(fingerprint) + "'"; | ||
| StatisticsUtil.execUpdate(sql); |
There was a problem hiding this comment.
[P2] Wait for deletion visibility before completing the pending load
execUpdate also succeeds when DELETE returns SQL OK with COMMITTED after its publish timeout (DeleteJob.commit; the MOW insert path does too by default). If this FE's one-time load is still pending after startup/retry, DELETE then invalidates the cache and acknowledges success, but the subsequent loadAll can still read the old visible row. ensurePinnedLoaded reinstalls it and marks the load complete, so the deleted override stays active even after publication removes the stored row. Wait for VISIBLE or preserve deletion state across that pending load; the FE-local lock alone does not establish SQL visibility. This needs neither another FE/replacement nor a caught SQL error, unlike the existing persistence threads.
| tables.add(scan.getTable()); | ||
| } | ||
| Collections.sort(tables); | ||
| pinnedRelationKeys.add(String.join(",", tables)); |
There was a problem hiding this comment.
[P3] Retire relation keys when pinned entries leave the cache
The pinned cache is bounded by hbo_pinned_stats_cache_num (5000 by default), but this set only grows: successful DELETE, stale cleanup and Caffeine eviction never remove relation keys. Repeated SET/DELETE for distinct table combinations therefore retains one full table-list string per historical combination for the lifetime of the FE, even when the pinned cache is empty. Load likewise registers rows beyond the cache's eventual admission bound. Bound/rebuild this index or track references from live entries so the configured cache bound also bounds its retained metadata. This is separate from the existing learned relation-table cache's hot-configuration issue.
### What problem does this solve?
Problem Summary: the previous commit introduced the length prefixed expression key but the condition
collections of a filter, an aggregation and a join still wrote the rendered text of their expressions
(and the join case was formatted with the new mark-join flag, so the replacement of that site did not
apply). The array literal case was therefore still one key for `['a','b']` and for the singleton array
whose element is `a:TEXT),lit(b`: the elements of a composite literal were only separated by the printed
grammar again.
The three sites now write the structural expression key as well, which also restores the identity
suffixes the display name carries (a udf's scope and id, a match analyzer, a search dsl, a lambda's
arguments, the distinct flag), the variant sub path of a slot (`v['age']` is a different column) and the
sorting/de-duplication of the conditions over their bytes. A corpus case pins the two arrays apart.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- The new corpus case `filter_with_an_injected_delimiter_array_literal` asserts the two arrays keep
different fingerprints in both literal modes; 201 cases of the stats package plus the HBO command
and pinned-race suites pass.
- Behavior changed: Yes - the fingerprints of the condition sites moved once (the display form did not).
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of #69322 at d54197a. Two new P2 fingerprint findings are reported inline: computed project outputs lose their bounded producer, and equivalent slot equalities receive different structural hashes. Existing threads and replies were checked; known issues are not reposted. No existing P0/P1 finding applies, so existing_blocking_comment_ids is empty. There was no additional user-provided focus.
Critical checkpoint conclusions:
- Goal and correctness: structural HBO keys, manual controls, freshness and collection are wired end to end; the two inline issues still break correct matching/reuse. Tests exist but do not cover these paired plans.
- Scope: broad, related FE feature work; physical-state preservation and legacy test moves support the same change. No unrelated implementation found.
- Concurrency: planner-local memo state and concurrent caches separate reads from the FE-local persistence/load monitor. SQL runs under that monitor; no new lock inversion established. Cross-FE stale replacement and delayed DELETE visibility remain existing threads 4336321664/4356183531.
- Lifecycle: per-query descriptions end with the memo; mutable holders are copied independently; startup loader retries and stops after successful loading. No cross-TU static initialization applies.
- Configuration: ratios and persistence are read dynamically; pinned size is explicitly startup-only; max-scans is session-forwarded. The relation-table cache hot-update omission remains existing 4336321670.
- Compatibility: no new FE/BE protocol or user-table storage format. Structural keys change; internal table schema checks fail visibly on missing columns and require operator recreation rather than automatic migration.
- Parallel paths: logical/physical readers, exact/agnostic and pinned/learned lookup, cloud startup, local SHOW and cost consumers checked. Skew costing freshness remains existing 4322187125.
- Conditional guards: FILTER_SMALL, stale fallthrough, unsupported scans/partial stages and numeric validation examined. Unknown-stat expansion and residual NULL selectivity remain existing 4355881604/4356183525.
- Test coverage: unit/golden and injection/drift/persistence regressions inspected. Real restart loading, cross-FE mutation, delayed publication and actual profile-feedback reuse lack execution evidence in this static review.
- Test results: supplied persistence output agrees with its SELECT fields/values on inspection; generation and pass status were not verified. The historical cluster corpus does not recompute current keys, and synthetic window-alias fixtures do not establish valid analyzed output bindings; the existing equality-only expansion assertion issue remains 4355881610.
- Observability: EXPLAIN/SHOW expose fingerprints, baseline, use/skip state; store/loader failures log context. Stale-cleanup failure acknowledgment remains existing 4354711309. No separate missing-metrics defect established.
- Persistence and transactions: normal internal SQL owns durability; memory mutations follow successful writes; one-time loading merges with putIfAbsent. Existing visibility/replacement threads limit restart/failover guarantees.
- Data writes and crashes: only HBO internal-table writes are added. Failed direct persistence preserves local entries and reports failure; no user-row visibility, delete-bitmap or BE memory-accounting code changes. Crash/reload behavior was not executed.
- FE/BE variables: no new transported field/operator; runtime feedback reads existing counters and planning snapshots use real PlanNodeIds. Learned row-population/strict-version and observation-mixing concerns remain existing threads 4322187106/4322187116/4336321656.
- Performance and memory: empty-cache/relations gates and cached summaries bound routine key work; pinned relation-index retention is existing 4356183536. No additional proven performance defect beyond reported matching issues.
- Other issues and limits: known aggregate-producer, NULL-path, MAP/cast/order and learned-wildcard threads were treated as duplicate fences. appendExpressionKey now recursively encodes ARRAY elements, but TopN still hashes rendered header text and remains covered by existing ARRAY thread 4354711292; builtin DISTINCT mapping flags are encoded in structural expression keys. No source coverage gap or unresolved candidate remains. No builds, unit tests, regression runs or source modifications were performed.
Review converged after 3 round(s).
| Map<ExprId, Expression> definitions = new LinkedHashMap<>(); | ||
| for (NamedExpression output : project.getProjects()) { | ||
| if (output instanceof Alias) { | ||
| definitions.put(output.getExprId(), |
There was a problem hiding this comment.
[P2] Keep computed project outputs bound to their producer
Consider this reduced supported plan over t(v)={0,1,1}:
Aggregate(group by a.x) // compare grouping by b.x
Cross
Project(v+1 AS a.x) -> TopN(v ASC, 2) -> Scan(t)
Project(v+1 AS b.x) -> TopN(v DESC, 2) -> Scan(t)
Both definitions stored here become Add(col(t.v),lit(1)). resolveNodeExpressions therefore gives either parent the same grouping expression and fingerprint in both literal modes, although a.x has two distinct values and b.x only one. The common children and scan baselines match, allowing an exact pin for the two-group query to override the one-group query. Preserve the row-selecting producer when propagating project definitions and cover this paired tree. This is distinct from the existing aggregate-output producer thread: neither branch contains an aggregate, so fixing aggregateColumnDefinitions alone leaves this case.
There was a problem hiding this comment.
扩展 nullextended的方法,使用child-index的方式标记计算expressoin的生成节点. 所有计算expr 都使用 bound([child-index], 表达式) 的方式记录
| out.text(expressionName(expression)); | ||
| List<Expression> children = expression.children(); | ||
| out.count(children.size()); | ||
| for (Expression child : children) { |
There was a problem hiding this comment.
[P2] Normalize commutative children before hashing
For Filter(t.a=t.b)->Scan(t) versus Filter(t.b=t.a)->Scan(t), the canonical renderer sorts the EqualTo operands, but this traversal writes their original order into the structural bytes. Upstream NormalizeBinaryPredicatesRule and SimplifyComparisonPredicate only commute constant/literal-left comparisons, so two distinct same-type slots retain that order. These equivalent filters consequently have different fingerprints in both modes despite identical printed structs and scan baselines, and a pinned count copied for one query misses the reversed query. Sort structural child keys for the commutative operators already recognized by isOrderInsensitive and add actual-fingerprint assertions for the reversed-column pair.
There was a problem hiding this comment.
表达式使用结构化IR后, 仍然需要做文本IR时做过的排序工作. 补充结构化IR的排序
…it reads
### What problem does this solve?
Problem Summary: the definition of a computed column kept only the expression, not the sub tree which
produces the rows it reads, so two plans whose definitions share the expression rendered one key. With
t(v) = {0,1,1}:
Aggregate(group by a.x) Aggregate(group by b.x)
Cross Cross
Project(v+1 AS a.x) -> TopN(v ASC, 2) -> Scan(t)
Project(v+1 AS b.x) -> TopN(v DESC, 2) -> Scan(t)
both definitions become `Add(col(t.v),lit(1))` and both grouping keys render the same though `a.x` has
two values and `b.x` one: the Cross (which names both branches) is the same in both plans, so nothing in
the key says which branch the aggregation groups by. An exact pin of the two group query therefore
applied to the one group query.
A computed definition which reads the output of a node that selects the rows (a top-n or a limit) is now
bound to that producer: the marker records the child indices from the producer to the consumer, which is
the same structural code the null extension marker uses. `NullExtended` becomes the general `Bound`, with
the null extension as the case where the crossing is gated on the definition being able to turn NULL into
a non NULL value; a row selecting boundary binds every definition. The code, a property of the assembled
tree, is the same whether the child was described fresh or reused from the memo.
### Release note
An hbo statistics entry of a plan whose computed column is bound to a top-n or a limit is no longer
applied to the plan computing the same expression over another such producer.
### Check List (For Author)
- Test: Unit Test
- The regenerated corpus renders `Bound(...)` for the null extension pair case; 201 cases of the stats
package plus the HBO command suite pass. A corpus case for the top-n pair needs the top-n fixture and
is a follow-up.
- Behavior changed: Yes - a computed column above a top-n or a limit is bound to that producer.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of #69322 at 982056e (base snapshot 7a7e86c). Three new P2 findings are inline: temporary-partition identity collisions, missing storage-aggregate relations in the lookup summary, and lost sharing of volatile evaluations. These can misapply row-count overrides or make an advertised override inapplicable.
The 117 authoritative changed paths and their relevant upstream/downstream flows were covered by the main reviewer, two complete-review agents and an additional identity-risk reviewer. Existing threads and replies were duplicate fences. Current fixes for structural ARRAY boundaries, DISTINCT, mark joins and global equality-pair orientation were verified; remaining previously reported issues were not reposted. There are no existing P0/P1 inline comments to carry in existing_blocking_comment_ids. No additional user focus was supplied.
Critical-checkpoint conclusions:
- Goal and proof: group-based identities, pinned/learned lookup, management and profile publication are connected, but the three inline cases violate intended identity/lookup behavior. Existing tests omit these cases.
- Scope and reuse: this is a broad, coherent FE HBO feature; common key, state-copy and internal SQL facilities are reused. Mechanical physical-plan changes were reviewed with their callers.
- Concurrency and locks: optimizer groups are query-local; asynchronous profile publication consumes captured keys. Loader and SET/DELETE share pinnedLoadLock and local stale removal compares entry identity under that lock. Internal SQL runs inside this dedicated lock. Previously reported publication-visibility and cross-FE replacement races still apply; no additional deadlock was established.
- Lifecycle: group caches, physical-state copies and planning-time snapshots preserve ownership through postprocessing. The background loader retries a pending one-time per-FE load. Existing relation-index retention remains known. No C++ static-initialization change is involved.
- Configuration: ratios and persistence are consulted live; the pinned-cache bound is explicitly startup-only. Session scan limits are forwarded. The existing relationTableIdCache hot-update omission remains known.
- Compatibility: no new thrift layout or BE wire field is introduced. Removed fingerprint APIs have no remaining FE source/test callers. Structural keys intentionally replace old keys; the internal table checks required columns and rejects an older incompatible schema rather than migrating it.
- Parallel paths: logical/physical plans, filters/joins/aggregates, pinned/learned/expansion consumers, file/internal scans, partial/final aggregation and cloud version access were inspected. The missing storage-wrapper summary is reported inline. Existing learned/skew freshness discrepancies remain duplicate-fenced.
- Conditions and invariants: entry-type, literal-mode, finite-number, overflow, scan-limit, freshness and FILTER_SMALL checks were traced to concrete inputs. Equal partition counts do not prove whole-table selection, and volatile evaluation sharing must survive normalization, as the inline findings show.
- Test coverage: all changed unit classes, 85 PLAN/51 TEXT golden cases, 21 historical captured cases, 11 regression suite bodies and eight exact legacy renames were inspected. Supplied tests do not exercise restart/load failures, cross-FE publication races, or the three new findings. The local race unit test uses persistence disabled.
- Test results: the new persistence .out agrees with its declared SET/DELETE operations. Golden output characterizes the implementation rather than independently proving semantics; historical cluster hashes are not recomputed as current structural keys. The already-reported equality-only expansion versus residual-literal regression assertion remains unresolved. No builds, unit tests, regression tests or live FE experiments were run, as required by the review contract; generated-result provenance was not independently established.
- Observability: SHOW baseline/state/detail, last-use information, EXPLAIN used/skipped markers and identifier-bearing store/snapshot logs are present. Existing stale-cleanup failure suppression remains known; the storage-summary finding explains a displayed key that silently cannot apply.
- Persistence and failover: pinned metadata uses internal OLAP SQL transaction/journal machinery rather than a separate EditLog format. Each FE keeps one local loaded snapshot. Previously reported deletion visibility, replacement and failure-reporting defects still apply; runtime failover was not exercised.
- Data writes and atomicity: direct SET/DELETE perform persistent work before cache mutation and report SQL failures. Stale cleanup's suppressed failure is already reported. The PR changes optimizer metadata and estimates, with no direct change to committed-row visibility, BE version bounds or MoW delete-bitmap handling.
- FE/BE variable passing: runtime publication reads existing counter fields; added identity, display and management state stays in FE. No new BE parameter requires parallel send-path updates.
- Performance and memory: group memoization, empty-cache/relation prefilters, scan caps and bounded caches were checked. The existing unretired relation-key index and missed learned-cache reconfiguration remain known. Full persisted-table startup loading is documented; no additional material performance defect was substantiated.
- Other risks and limits: every candidate and initial risk has an explicit accepted, duplicate or dismissed disposition in the ledger. Suspected raw VARIANT expansion collisions were dismissed because raw VARIANT equality joins are rejected and cast operands have no expansion key. Remaining validation limits are static-only review, absent runtime race/failover proof and historical captured-plan evidence; no unresolved new candidate is being asserted as a finding.
Review converged after 2 round(s).
| * the scan is the whole table and its identity does not have to name a selection). | ||
| */ | ||
| static String partitionIdentityOf(List<Long> selectedPartitionIds, long totalPartitions) { | ||
| if (selectedPartitionIds.size() == totalPartitions) { |
There was a problem hiding this comment.
[P2] Keep temporary partition IDs in the scan key
Consider Aggregate(gb=t.v) -> Scan(t TEMPORARY PARTITION(tp1)) versus the same tree reading tp2, with one ordinary partition p and two temporary partitions. Let p have two rows, tp1.v={0,1} and tp2.v={0,0}. of() passes the ordinary-only getPartitionNames().size(), so both one-partition selections take this branch and lose their IDs. Both literal modes then have identical fingerprints despite two versus one groups. Temporary IDs also miss getRowCountForPartitionIndex and receive the same estimated ordinary-table baseline, so freshness accepts the other population's exact pin. Retain selected IDs unless their set actually equals the ordinary partition-ID set, or reject unsupported temporary scans, and cover this paired plan.
There was a problem hiding this comment.
temporary partition 上查询是非常少见的操作. 不收录hbo信息
| Plan plan = ge.getPlan(); | ||
| List<String> relations = new ArrayList<>(); | ||
| int scanCount = 0; | ||
| if (plan instanceof OlapScan) { |
There was a problem hiding this comment.
[P2] Count storage-aggregate scans in the relation summary
For an indexed DUP/MOW table, the default count-on-index implementation can produce Aggregate -> Filter(predicate) -> PhysicalStorageLayerAggregate(COUNT_ON_MATCH, embedded Scan(t)). pushdownCountOnIndex creates a new filter memo group, and buildNode includes the embedded scan in its full struct. This summary recognizes neither the storage wrapper nor its embedded relation, however, so that filter's relation key is empty. Copying its advertised EXPLAIN key into HBO SET registers relation t; applyPinnedStats then rejects the empty summary key before fingerprint lookup, so the pin never applies. Include the embedded storage relation consistently with buildNode and add a filter-over-storage-summary/lookup case.
There was a problem hiding this comment.
用PhysicalStorageLayerAggregate内部的olapScan 代替它计算hbo
There was a problem hiding this comment.
已按“用内嵌 relation 代替 wrapper 计算 HBO 信息”的方式修复(commit 见最新 push):
describeExpression对PhysicalStorageLayerAggregate直接描述它内嵌的PhysicalOlapScan/PhysicalFileScan(getRelation()),canonical 现在是S{...}而不是A{sl,op=...}(S{...});summaryOf增加对应分支:scanCount = 1且把内嵌 relation 记入relations,使摘要与buildNode/collectScans一致(这正是空摘要导致 pinned 预过滤误杀的根因);- wrapper 自身仍然不参与发布/广告(
hasNoHboLookupPath保持不变),可应用的键是它上层节点的键。
验证:org.apache.doris.nereids.stats.* + golden corpus + HBO 命令套件全绿。补充说明:真正构造 PhysicalStorageLayerAggregate 的单测还需要 physical scan 的测试脚手架(PlanConstructor 目前没有),这条用例留作后续补充。
| Map<ExprId, Expression> definitions = new LinkedHashMap<>(); | ||
| for (NamedExpression output : project.getProjects()) { | ||
| if (output instanceof Alias) { | ||
| definitions.put(output.getExprId(), |
There was a problem hiding this comment.
[P2] Preserve sharing of materialized volatile evaluations
Compare the supported reduced trees Filter(strcmp(x,x)=0) -> Project(uuid() AS x) -> Scan(t) and Filter(strcmp(uuid(),uuid())=0) -> Scan(t). The first compares one stored value with itself and keeps every row; the second compares independent random UUIDs and ordinarily keeps none. This definition substitutes the same UUID expression for both x references, the transparent project disappears, and appendExpressionKey encodes each leaf only as Uuid, losing the VolatileIdentity distinction between shared and independent calls. Both fingerprints therefore coincide in both modes, and identical scan baselines allow an exact pin to cross between them. The normal pushdown rule deliberately preserves volatile aliases. Preserve a stable evaluation-sharing binding, or reject these descriptors; this is distinct from the existing row-selecting-producer thread and needs no TopN/Limit/aggregate/join.
…ural key
### What problem does this solve?
Problem Summary: the canonical renderer sorts the operands of a commutative expression, but the
structural key wrote the children of every expression in their written order. `NormalizeBinaryPredicatesRule`
and `SimplifyComparisonPredicate` only commute constant-left comparisons, so `Filter(t.a = t.b)` and
`Filter(t.b = t.a)` rendered one struct and hashed two: a pinned row count copied from one query was not
found by the query which only reverses the two columns.
The structural key now normalizes the operators the renderer already recognizes as order insensitive
(equality, null safe equality, and, or): each child is serialized on its own, the child keys are sorted by
their unsigned bytes, and the sorted keys are written length prefixed. The code does not read the rendered
text, so two operands whose text would tie still have a deterministic order. The corpus fingerprints of
plans whose written operand order differed from the sorted one move once.
### Release note
An hbo statistics entry of a plan with a commutative expression inside a filter or a join condition is no
longer sensitive to the written operand order.
### Check List (For Author)
- Test: Unit Test
- A test asserts `Filter(t.a = t.b)` and `Filter(t.b = t.a)` share the fingerprint in both literal
modes; the regenerated corpus (70 fingerprint lines) and 201 cases of the stats package plus the
HBO command suite pass. The regression suites were not re-run against a FE rebuilt from this commit.
- Behavior changed: Yes - the structural key sorts the children of a commutative expression.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of PR #69322 at b569fd2: two new P2 findings: aggregate-combinator identity and empty-key VARIANT subpath identity in HBO keys. Existing threads and replies were revalidated as duplicate fences; no existing P0/P1 finding applies. The commutative-child and ordinary aggregate-DISTINCT encoding fixes are recognized. Still-applicable descriptor, learned-statistics, persistence, cache-index and regression concerns remain in their original discussions and are not reposted.
Critical checkpoint conclusions:
- Goal and proof: the change implements structural HBO keys, manual statistics, freshness checks and persistence. Unit, golden and regression coverage exercises the main flow; the inline combinator and empty-key VARIANT cases are missing and prevent complete key correctness.
- Scope and reuse: this is a coherent but broad FE feature. Shared descriptor/store helpers serve its consumers; physical state-copy changes support key recovery after postprocessing. Eight legacy suites move without content changes.
- Concurrency and locks: optimizer groups stay on the planner thread. Admin commands and the loader share pinnedLoadLock; persistence/load SQL runs inside that local monitor, while query lookup reads caches. Profile publication uses provider caches. No new catalog-lock ordering issue was found; the existing SQL-visibility and cross-FE replacement concerns remain.
- Lifecycle/static initialization: group descriptions live with the memo; node-key snapshots survive through expiring provider maps. The startup loader retries failed loads and stops after success or internal-schema disable. There is no C++ static initialization change. Historical relation-index retention is already reported.
- Configuration: row/filter thresholds and persistence switch are read dynamically; pinned cache size is explicitly startup-only. Session scan limits and EXPLAIN display are honored. The existing learned relation-cache hot-rebuild omission remains.
- Compatibility/rolling versions: structural keys replace legacy keys; the new private internal table verifies its schema and rejects older incompatible layouts. No BE wire format or function ABI changes. Mixed-version key continuity is not established by runtime validation.
- Parallel paths: logical/physical plans, literal modes, pinned/learned lookup, profile publication, storage wrappers, supported file scans, join types and Java/Python UDF identities were inspected. Existing path-specific issues remain fenced.
- Conditional checks: unsupported/partial/storage paths, scan-count gates, small-filter guard, missing-input checks and freshness states have explicit fallback/skip behavior; no additional distinct conditional defect was verified.
- Test coverage: positive/negative key, scan, freshness, command, profile-counter, input-alignment and local replacement cases are present. Both new pairs need actual-fingerprint coverage. Restart, multi-FE and runtime behavior were inspected statically only.
- Expected results: golden producer/assertion code, both corpora and the persistence .out were reviewed; captured historical cluster data is not current execution proof. The already-reported equality-only expansion regression assertion still conflicts with the implementation. Generated-result provenance was not independently executed.
- Observability: EXPLAIN keys/use/skip/expansion annotations, SHOW state/baseline/last-use details and persistence/snapshot logs cover the new decisions. Stale-cleanup client error reporting remains an existing issue; no further observability finding was verified.
- Persistence/failover: changes use ordinary internal-table SQL rather than new EditLog records. Startup reload and local replacement checks were traced; previously reported visibility and replacement limitations remain. Failover execution was not tested.
- Data writes/atomicity: writes affect HBO metadata only. User transaction publication and delete-bitmap execution are unchanged; command cache updates follow SQL success, subject to the existing publication-visibility concern.
- FE/BE variables: the new controls are FE-side; the scan-limit setting has forwarding metadata. No new thrift field or BE execution parameter requires propagation.
- Performance: lazy group summaries, memoized descriptions, scan limits and empty-cache/relation gates bound common lookup work. Initial full-table loading and monitor-held SQL were inspected; historical relation-index growth is already reported. No new independent performance defect was proved.
- Core invariants/error handling: committed/uncommitted data visibility and BE memory ownership are unchanged. Cloud visible-version access and error propagation were traced; known learned-freshness and stale-cleanup error limitations remain in existing threads.
- Other issues/user focus: complete changed-file and upstream/downstream sweeps found only the two new inline issues beyond existing discussions. There was no additional user-provided focus. No unresolved candidate or unreviewed assigned path remains.
Validation limit: static review only, as required. No source changes, builds, Doris unit/regression runs or live concurrency experiments were performed. Convergence describes the review process, not resolution of the reported bugs.
Review converged after 3 round(s).
| // arguments keep a different number of rows in another time zone | ||
| return className + '<' + encodeSlotComponent(sessionTimeZone()) + '>'; | ||
| } | ||
| return className; |
There was a problem hiding this comment.
[P2] Preserve the nested function in aggregate-combinator keys
Compare the supported reduced trees Filter(array_contains(x,0)) -> Aggregate(gb=k, min_foreach(a) AS x) -> Scan(t) and the same tree with max_foreach(a). For t(k,a)={(1,[0]),(1,[1])}, the first keeps one group (x=[0]) and the second keeps none (x=[1]). Both calls are ForEachCombinator with the same ARRAY child; their nested Min/Max function is stored outside children(). This fallback therefore gives both the same expression identity. aggregateColumnDefinitions substitutes that identity into the filter, so both actual fingerprints coincide in both literal modes and identical scan baselines let an exact pin apply the other query's row count. Encode the combinator's nested function identity/bound name and cover this paired filter. This is distinct from the existing Cast/OrderExpression and producer-binding issues: neither type nor producer differs here, and their fixes cannot distinguish these functions.
There was a problem hiding this comment.
需要将expression中非child的部分写入expr的IR结构
There was a problem hiding this comment.
Fixed in the latest commit: see the corresponding change in GroupStructInfo/HboJoinConditions (counted qualifier and sub path components, VARCHAR-folded literal type in the NO_LITERAL key, escaped operand components, and the for-each combinator's nested function name in its identity).
| out.text(String.join(".", slot.getQualifier())); | ||
| out.text(slot.getName()); | ||
| // a variant column read through a sub path (`v['age']`) is a different column | ||
| out.text(String.join("\u0000", slot.getSubPath())); |
There was a problem hiding this comment.
[P2] Encode VARIANT subpaths as a counted sequence
With the default element-at-to-slot rewrite enabled, compare Filter(IsNull(v)) -> Scan(t) and Filter(IsNull(v[''])) -> Scan(t). Empty object keys are supported: VariantSubPathPruning accepts the empty string, and LogicalOlapScan.computeOutput builds the extracted slot with the same name/qualifier as v and subpath [""]. For t.v={SQL NULL, {"":1}, {"a":2}}, the first filter keeps one row and the second keeps two. This join encodes both the base path [] and [""] as the empty string; every other structural field is identical, so both actual fingerprints collide in both modes despite different displayed canonicals. Identical scan baselines then allow an exact pin for one filter to override the other's estimate. Write the path length and each component separately, and cover the base-column/empty-key pair with actual-fingerprint assertions.
There was a problem hiding this comment.
需要加count和长度前缀 到subpath的编码中
There was a problem hiding this comment.
Fixed in the latest commit: see the corresponding change in GroupStructInfo/HboJoinConditions (counted qualifier and sub path components, VARCHAR-folded literal type in the NO_LITERAL key, escaped operand components, and the for-each combinator's nested function name in its identity).
### What problem does this solve?
Problem Summary: a temporary partition is a short lived copy which a manual job replaces, and the queries
which read one are rare, so a key recorded for it is not worth the risk of a stale match; the partition
identity was also not part of the scan key in every path, which made two scans of one table differ in
places but match in others.
An olap scan whose selected partitions contain a temporary partition now refuses the description with the
reason `temporary partition of <table>`, like a tablesample does: no struct info, no fingerprint and
therefore no pinned or learned entry is generated for that sub tree, and nothing it feeds into can be
applied. The refusal is reported by EXPLAIN and by the `HBO SHOW` verdicts.
### Release note
An hbo statistics entry is no longer injected for a query which reads a temporary partition.
### Check List (For Author)
- Test: Unit Test
- 202 cases of the stats package and the HBO command suite pass. A case with a temporary partition
needs the olap table fixture of the catalog tests and is a follow-up.
- Behavior changed: Yes - a scan of a temporary partition is refused instead of being given a key.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of #69322 at 1f1e778. Five new P2 findings are attached inline: qualifier-boundary collisions, failed string parameterization, ambiguous expansion operands, diluted selected-population freshness, and lost self-join multiplicity. They affect HBO matching or estimates, not a demonstrated change to returned SQL rows.
Reviewed flow: HBO SQL -> manager/cache/internal-table persistence -> startup loader; memo statistics -> structural key -> pinned/learned guards -> estimates; selected physical plan -> node-ID key snapshot -> runtime profile -> learned publication.
Critical checkpoint conclusions:
- Goal and proof: structural identity, manual management and feedback are connected end to end, but the five cases show incomplete matching/freshness guarantees. Relevant unit, golden and regression tests were inspected; these cases are missing.
- Scope and reuse: the change is broad but stays within HBO/planner identity and its tests. Plan reset methods reuse the common mutable-state copy helper.
- Concurrency and locks: optimizer/memo work is single-threaded; SQL sessions, background loading and profile publication are separate concurrent origins. The local pinned-load monitor serializes load/SET/DELETE and includes synchronous SQL, so management calls may wait on IO. Caffeine/concurrent containers and immutable entry fields protect shared reads; no additional lock-order cycle was established.
- Lifecycle: query memo descriptors and propagated group identity feed immutable planning-time fingerprint snapshots. The loader retries until a successful one-time load and avoids starting a second live copy. No C++ static-initialization change applies.
- Configuration: filter/drift ratios and session controls are read dynamically; pinned cache size is explicitly startup-only. The learned relation-table cache's hot-config omission is already reported.
- Compatibility: legacy fingerprint identities change; old internal schemas are detected and rejected rather than migrated. Restart/rolling-schema behavior was inspected statically only. No new FE-BE wire field is introduced.
- Parallel paths: checked logical/physical plans, postprocessing copies, join/aggregate/filter lookups, expansion costing, cloud/shared-nothing loading and internal/external scan treatment. The existing CostModel freshness issue is not reposted.
- Conditional guards: unsupported selectors/temporary partitions and scan-count limits are explicit. Missing fingerprint/input snapshots prevent unsupported wildcard publication; known learned guard defects remain existing-thread context.
- Test coverage: unit/golden cases cover key composition, identity, guards and exclusions; regression cases cover injection, drift, display and basic persistence. Restart, race, cloud execution and the new witnesses were not executed under the static-only contract.
- Changed results: the persistence .out baseline matches its stated deterministic SET/DELETE cases by inspection. The captured cluster corpus is historical and checks formatting/folding, not current runtime keys. The already-reported residual-expansion regression mismatch remains separate.
- Observability: EXPLAIN/SHOW expose keys, selected modes and guard decisions; persistence failures are logged. Stale-cleanup failure acknowledgement is already reported.
- Persistence/failover: internal SQL provides storage durability; no new EditLog record is added. FE-local serialization does not establish cross-FE replacement ordering or deletion visibility; existing threads already cover these cases.
- Writes and crash behavior: cache publication follows successful persistence calls. Existing publication/deletion visibility issues prevent a stronger atomicity claim; no user-data transaction/storage-write change is introduced.
- FE/BE variables: new controls act in FE planning; no additional transport path was required.
- Performance and memory: scan gates, memoization and cache bounds were reviewed. Relation-index growth is already reported; no additional concrete performance regression was established.
- Other invariants: committed-row visibility, delete bitmaps and BE allocator ownership are outside the changed execution/storage paths. The new findings concern FE estimate correctness.
Existing review threads and all raw replies were used as duplicate fences, including outdated anchors omitted from the thread markdown. Confirmed existing issues were not reposted; no existing P0/P1 comment applies, so existing_blocking_comment_ids is empty. User focus: no additional focus was supplied. Coverage includes all 117 changed paths and each initial risk focus. Validation is static only: no builds, tests, source changes or runtime SQL were performed.
Review converged after 2 round(s).
| } | ||
| if (expression instanceof SlotReference) { | ||
| SlotReference slot = (SlotReference) expression; | ||
| out.text(String.join(".", slot.getQualifier())); |
There was a problem hiding this comment.
[P2] Encode qualifier components separately in the slot key
With enable_nested_namespace=true, consider the supported reduced trees Aggregate(gb=z.v) -> CROSS(FileScan(cat.[x.y].t AS z), FileScan(cat.x.u AS [y.z])) and the same tree grouping by [y.z].v (brackets denote quoted identifiers). For t.v={0,1} and u.v={0,0}, they have two versus one groups. LogicalSubQueryAlias.computeOutputInternal gives those slots qualifiers [cat,x.y,z] and [cat,x,y.z]; alias-to-project preserves them. This join writes both as cat.x.y.z, with the same slot name/type, so their actual fingerprints coincide in both modes even though the displayed canonicals differ. The common child and relation summaries are identical, and unversioned file scans permit pins with unknown freshness, allowing the other aggregate's count to apply. Encode the qualifier count and each component separately. This is independent of the existing VARIANT-subpath thread: neither slot has a subpath.
There was a problem hiding this comment.
Fixed in the latest commit: see the corresponding change in GroupStructInfo/HboJoinConditions (counted qualifier and sub path components, VARCHAR-folded literal type in the NO_LITERAL key, escaped operand components, and the for-each combinator's nested function name in its identity).
| out.text(String.join("\u0000", slot.getSubPath())); | ||
| } else if (expression instanceof Literal) { | ||
| Literal literal = (Literal) expression; | ||
| out.text(String.valueOf(literal.getDataType())); |
There was a problem hiding this comment.
[P2] Remove value-derived string lengths from NO_LITERAL keys
Compare Filter(Like(t.s,'%x_%')) -> Scan(t) with the same tree using '%long_%'. visitStringLiteral produces VARCHAR(4) and VARCHAR(7) literals, and compatible string coercion preserves those lengths. Both printed NO_LITERAL structs contain Like(col(...s),lit(*)), but this unconditional type field makes their actual structural fingerprints differ. Consequently a NO_LITERAL pin for the first pattern silently misses the second despite identical scans; the advertised constant-agnostic string parameterization only works for patterns of the same length. Give literals a mode-aware key that removes value-derived length/type distinctions when folding them, and add actual-fingerprint assertions for different-length string parameters.
There was a problem hiding this comment.
Fixed in the latest commit: see the corresponding change in GroupStructInfo/HboJoinConditions (counted qualifier and sub path components, VARCHAR-folded literal type in the NO_LITERAL key, escaped operand components, and the for-each combinator's nested function name in its identity).
| forward.add(pair); | ||
| backward.add(pair); | ||
| } else { | ||
| forward.add(leftSide == 0 |
There was a problem hiding this comment.
[P2] Encode expansion operands before assembling pair text
Raw quoted column names can forge the operand boundary before fingerprintOf length-prefixes the complete pair. For tables internal.db.l and internal.db.r, name left column A a),col(internal.db.r.b and right column B b),col(internal.db.r.z (supported with the default Unicode-name setting). Compare these reduced INNER joins:
common: l.a=r.c1, A=r.c2, l.c3=B, l.c4=r.z
extra: A=r.z -- versus l.a=B
Both extra pairs become exactly EqualTo(col(internal.db.l.a),col(internal.db.r.b),col(internal.db.r.z)). The four common pairs make the operand sets identical too, so all key fields and actual fingerprints collide. In positional columns (a,A,c3,c4) / (c1,c2,B,z), give both tables rows (0,0,0,0),(0,1,1,1),(1,0,1,0),(1,1,0,1): the joins return four versus two rows despite identical marginal column statistics. A pinned expansion therefore crosses different equality populations without another guard. The supported setting disable_nereids_rules='INFER_PREDICATES,CONSTANT_PROPAGATION,REORDER_JOIN,REWRITE_JOIN_EXPRESSION' preserves these five direct hash predicates; duplicate elimination removes none. Encode comparison kind and each operand separately, then render the readable text. This differs from the existing table-binding/self-join threads: both sides are correctly bound to different named tables here.
There was a problem hiding this comment.
Fixed in the latest commit: see the corresponding change in GroupStructInfo/HboJoinConditions (counted qualifier and sub path components, VARCHAR-folded literal type in the NO_LITERAL key, escaped operand components, and the for-each combinator's nested function name in its identity).
| String table = HboScanDescriptor.qualifiedNameOf(scan.getTable()); | ||
| // the input rows of a scan node are the rows it read, i.e. the number a catalog row | ||
| // count (which also ignores the predicates) can be compared with | ||
| recordedRowsByTable.merge(table, input.getInputRows(), Long::sum); |
There was a problem hiding this comment.
[P2] Check distinct selected scan populations before summing
For INNER(a.k=b.k, Scan(t PARTITION(p1) AS a), Scan(t PARTITION(p2) AS b)), use a DUP table with every k=0 and runtime filters disabled. Learn the join with p1=10 rows and p2=10000 rows, then grow only p1 to 1,000 and wait for measured partition reports. The same aliases/index/partition IDs keep the fingerprint and FULL_MATCH unchanged. This merge and ofLearnedRows compare only table totals, 10010 versus11000 (+9.89%), so the default 10% tolerance accepts the old 100,000-row output although the join now has 10,000,000 rows and one selected input grew 100x. Each runtime count accurately measures its full selected population; no missing/zero or scan-counter discrepancy is needed. This loses the per-selected-scan tolerance described by this class and used by pinned freshness. Align by table/index/partition selection and check each distinct population before aggregating; unrelated stable scans should not dilute its drift. This requires no repeated-key merge and is distinct from the existing population-counter and output/input-pairing threads.
There was a problem hiding this comment.
learned hbo 要重做, 不在这个pr里修改
| } | ||
| Map<String, PlanStatistics> merged = new LinkedHashMap<>(); | ||
| for (PlanStatistics statistics : inputTableStatistics.get()) { | ||
| merged.merge(identityOf(statistics), statistics, InputTableStatisticsInfo::largerRows); |
There was a problem hiding this comment.
[P2] Preserve within-node scan multiplicity when merging feedback
Use a 1000-row DUP table and UNION ALL(Join(Scan(t a),Scan(t b),a.k=b.k), Join(Scan(t a),Scan(t b),a.k=b.k)), with identical local aliases and runtime filters disabled. The two hash joins have different runtime IDs but the same fingerprint, so genPlanStatisticsMap merges their inputs here. Each input list correctly contains two 1000-row occurrences; identityOf omits the occurrence, and this loop collapses even the first list to one before adding the second. The published entry records 1,000 rows, while the live join descriptor contains both aliases and freshness sums 2,000, rejecting feedback from the next unchanged query as 100% growth. Ordinary matching still passes because it clones the historical one-input list. Preserve the multiplicity within each node when aligning repeated measurements, or retain one complete matching measurement. This differs from the existing output/input-pairing thread: both branches have identical predicates, equal outputs and accurate full-scan measurements; the loss is the two occurrences within either one of them.
There was a problem hiding this comment.
learned hbo 要重做, 不在这个pr里修改
### What problem does this solve?
Problem Summary: `PhysicalStorageLayerAggregate` (the wrapper a count-on-index plan puts over the scan it
reads) was described as a node kind of its own, so the canonical string of the filter above it named the
scan (`F{...}(A{sl,op=COUNT_ON_MATCH}(S{t}))`) while the structural summary of that filter recognized
neither the wrapper nor its embedded relation: the wrapper is a catalog relation without children, so
`summaryOf` fell through both of its leaf branches and produced an empty summary. The pinned entry
lookup uses the summary as its necessary condition, so a pin copied from the EXPLAIN of that filter
registered the relation and was then rejected by the prefilter before its fingerprint was compared - the
entry was stored and never applied.
The hbo information of the wrapper is now the one of the relation it embeds: the description of the
group is the description of that scan, and the summary counts it as one scan of that table. The wrapper
itself is still not published or advertised (its translator delegates to the embedded relation and the
read side applies statistics at filter/join/aggregation nodes only), but the filter above it now has a
summary which matches its canonical string, so its pin can be found.
### Release note
A pinned hbo statistics entry of a filter above a count-on-index plan is applied again.
### Check List (For Author)
- Test: Unit Test
- The stats package, the golden corpus and the HBO command suite pass. A dedicated unit case which
builds a `PhysicalStorageLayerAggregate` still needs the physical-scan harness (no
`PlanConstructor` helper for it yet) and is left for a follow-up.
- Behavior changed: Yes - the canonical string of a group whose node is the storage wrapper is the scan
it reads, and the fingerprints of those groups moved once.
- Does this need documentation: No
### What problem does this solve?
Problem Summary: five fields of the structural key were written in a form which cannot be read back, so
two structures the key is supposed to separate collided (or an equivalent one was split):
1. A slot's qualifier was joined with `.` (`[cat,x.y,z]` and `[cat,x,y.z]` both became `cat.x.y.z`, two
groups against one for a grouping over such a slot).
2. A variant sub path was joined with NUL, so the base column (`[]`) and the empty key (`[""]`) both
became the empty string (`IsNull(v)` keeps one row of t.v={SQL NULL,{"":1},{"a":2}}, `IsNull(v[''])`
keeps two).
3. A string literal's type (VARCHAR(4) for '%x_%', VARCHAR(7) for '%long_%') was written even in the
NO_LITERAL key, so the constant agnostic key of two patterns of different length still differed.
4. The operand of an expansion pair was written raw, so a quoted column name containing `),col(` could
forge the boundary between two operands.
5. The nested function of `ForEachCombinator` is a field, not a child, so `min_foreach` and `max_foreach`
had the same identity although they keep different groups.
A qualifier and a sub path are now counted sequences of length prefixed components; a string literal folds
its type to VARCHAR in the NO_LITERAL key; an operand escapes `%`, `,` and the parentheses before the pair
text is assembled; and a for each combinator writes the name of its nested function (`min_foreach`).
### Release note
Hbo statistics keys distinguish quoted identifier components, variant sub paths, string patterns of
different length and aggregate combinators of different nested functions.
### Check List (For Author)
- Test: Unit Test
- The regenerated corpus (142 fingerprint lines) and 202 cases of the stats package plus the HBO
command suite pass. The paired fingerprint fixtures for the qualifier and variant cases need file
scan and variant slot fixtures and are follow-ups.
- Behavior changed: Yes - the five fields above are encoded without ambiguity.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of #69322 at 6cf266b. Three new P2 findings concern sampled physical scans, lost explicit tablet selectors, and incomplete learned input coverage. Existing threads were treated as duplicate fences; no existing P0/P1 finding requires a blocking-comment ID.
Critical checkpoint conclusions:
- Goal and end-to-end behavior: traced parsing and management, memo identity, pinned/learned lookup, costing, physical-plan snapshots, profile publication and EXPLAIN. The feature is wired through these stages, but the three inline findings leave population-validation gaps. Unit, golden and regression coverage exists; this static review does not establish runtime success.
- Scope and clarity: the change is a broad HBO feature. Physical-plan state preservation supports its memo/EXPLAIN integration; no unrelated source change was identified.
- Concurrency and locking: query-local memo construction is thread confined. Background loading, management statements, asynchronous feedback and hot-config replacement interact through concurrent caches and the FE-local load lock. No additional deadlock was substantiated. Existing cross-FE replacement and SQL deletion-visibility concerns remain covered by their threads.
- Lifecycle: descriptors live with the query; feedback uses planning-time fingerprint snapshots. The loader retries and exits after one successful load. Per-FE caches and one-time loading are intentional. Existing unretired relation-index metadata remains covered by its P3 thread.
- Configuration: ratio settings are read dynamically; persistence changes affect the documented local lifecycle; the pinned-cache size is explicitly startup-only. The existing relation-table-cache reconfiguration omission remains threaded.
- Compatibility: these changes introduce no FE-BE protocol fields. The new internal persistence table checks required columns and reports obsolete schemas. Rolling-upgrade execution was not validated.
- Parallel paths: logical/physical scans, embedded storage aggregates, pinned/learned consumers, exact/constant-agnostic modes, cloud version reads, and join expansion variants were inspected. The new findings identify gaps specifically in embedded physical scan handling.
- Special conditions and errors: unsupported shapes, temporary partitions, samples, scan limits and fallback paths were traced. Direct persistence failures surface to SET/DELETE callers. The existing stale-cleanup error-reporting concern is not repeated.
- Tests and negative coverage: inspected the changed unit/golden/regression tests and all eight unchanged legacy moves. The mixed ordinary/storage-scan feedback case and sampled/tablet-selected count-on-index paths are missing. Existing expression/producer identity, learned population/version/matching, and expansion defects remain in their original threads.
- Changed results: inspected the persistence result file against its query projections and the golden resources against their generators and assertions. Generation history and runtime results were not independently verified. The existing equality-only expansion regression assertion mismatch remains covered by its thread.
- Observability: EXPLAIN exposes fingerprints, structures and applied/skipped states; persistence and snapshot failures include diagnostic context. No additional observability issue was substantiated.
- Persistence, failover and writes: only optimizer overrides are written through the internal table; user-table transaction/visibility and delete-bitmap logic are not modified. Local replacement guards and startup reload were inspected, with the existing multi-FE/SQL-visibility limitations retained. Crash/failover behavior was not executed.
- FE/BE variable passing and memory: no new backend wire contract or C++ allocation/ownership change applies. The session scan bound is forwarded; EXPLAIN control remains local.
- Performance and reuse: empty-cache/relation prefilters, memoized descriptions and scan limits reduce unnecessary work. Wrong estimates from the inline findings can change plan selection. No additional independently triggered hot-path defect was confirmed.
- Additional concerns and user focus: no additional user focus was supplied. All 117 changed paths and the five initial risk areas received static coverage. Suspected UDAF DISTINCT and direct VARIANT-equality collisions were dismissed after checking upstream binding/coercion; general state-copy concerns were checked against actual postprocessor ordering.
No builds or tests were run and no repository source was modified, as required by this review's static-only contract. Convergence is evaluated separately from the review verdict; existing P2/P3 threads are not declarations that those defects are fixed.
Review converged after 3 round(s).
| invalid(ctx, "temporary partition of " + scan.getTable().getNameWithFullQualifiers()); | ||
| return null; | ||
| } | ||
| if (scan instanceof LogicalOlapScan && ((LogicalOlapScan) scan).getTableSample().isPresent()) { |
There was a problem hiding this comment.
[P2] Reject sampled physical scans in storage aggregate descriptions
COUNT_ON_INDEX can turn Aggregate(count(*)) -> Filter(p) -> Scan(t TABLESAMPLE(...)) on an indexed DUP/MOW table into a new Filter(p) -> PhysicalStorageLayerAggregate(COUNT_ON_MATCH, embedded PhysicalOlapScan(sample)) memo group. buildNode passes that embedded physical scan here, bypassing this logical-only guard. HboScanDescriptor.of then emits the unsampled scan identity and full-table baseline. A pin for ordinary Filter(p) -> Scan(t) consequently matches and passes freshness for the sampled query, although translation preserves the sample and OlapScanNode later selects sampled tablets. Reject sampled PhysicalOlapScan too, including embedded storage relations, and cover the count-on-index path.
There was a problem hiding this comment.
Fixed: a table sample now refuses the description for every scan class, and the storage aggregate path goes through the same check (the physical scan it embeds is covered), plus a sampled PhysicalFileScan is rejected like a snapshot.
| } | ||
| } | ||
| return new InputTableStatisticsInfo(Optional.of(planHash), Optional.of(inputTableStatisticsBuilder.build())); | ||
| if (recorded != scans.size()) { |
There was a problem hiding this comment.
[P2] Count embedded storage scans when validating feedback inputs
For Join(t.k=a.c, Scan(t), Aggregate(COUNT(*) AS c, StorageAggregate(COUNT, embedded Scan(u)))), default no-group aggregate pushdown holds u in getRelation() rather than children(). Both collectors above miss it, so this completeness check accepts feedback containing only t. With runtime filters disabled, learn when every t.k=100 and u has 100 rows; then append 100 rows only to u. The version-free join key still matches, matching checks only recorded t, and learned freshness accepts unchanged t, reusing |t| although the join now returns zero rows. This differs from the existing population/multiplicity threads: the second table is absent entirely. Traverse embedded relations and reject publication if any input cannot be recorded; include wrapped file scans in the external-input exclusion too.
| // only a TABLET(...) selection belongs to the identity: the effective tablet set of a plain scan | ||
| // (which the optimizer derives from the selected partitions and the index) is not an identity of | ||
| // its own, and naming it would move the key of every ordinary scan | ||
| List<Long> specifiedTabletIds = scan instanceof LogicalOlapScan |
There was a problem hiding this comment.
[P2] Preserve explicit tablet selectors in physical scan identities
For an indexed DUP/MOW table, compare Aggregate(COUNT(*)) -> Filter(p) -> Scan(t) with the same tree reading t TABLET(id) for a proper tablet subset. COUNT_ON_INDEX introduces a new filter group over an embedded PhysicalOlapScan. The converter preserves its selected tablets, but this physical branch always sets specifiedTabletIds empty. The new filter therefore loses #tb and matches the full-scan filter pin; freshness also passes because rowsOf counts the full selected partitions. Execution still restricts reads to the chosen tablets. This needs no TABLESAMPLE, so extending the sample guard does not fix it. Carry manual-tablet provenance into the physical descriptor, or decline an unsafe descriptor, and test the count-on-index TABLET/full-scan pair.
…ax_foreach The two combinators are one class with a nested aggregate function field, so the key has to write the nested function name; the paired filter over the aggregate now asserts the fingerprints differ in both literal modes.
### What problem does this solve?
Problem Summary: the description refused a `LogicalOlapScan` with a table sample, but a storage layer
aggregate embeds a `PhysicalOlapScan`, and a sampled physical scan passed the check: the sample reads a
random subset, different on every execution, so a row count recorded under it cannot be reused - and the
storage aggregate is described as the scan it embeds, so it inherited the sample. A physical file scan
with a sample was likewise accepted, because the check also only looked at the logical file scan.
A table sample now invalidates the description for both scan classes, and a sampled `PhysicalFileScan` is
treated like a snapshot or scan params (unsupported). The reason is reported as
`tablesample of <table>`, so no struct info, fingerprint, pinned or learned entry is generated for such a
sub tree.
### Release note
An hbo statistics entry is no longer injected for a query which reads a table sample, whatever the scan
class or the operator which embeds it.
### Check List (For Author)
- Test: Unit Test
- 203 cases of the stats package (including the new paired combinator case) plus the HBO command suite
pass. A sampled physical scan case needs the olap scan fixture and is a follow-up.
- Behavior changed: Yes - a sampled physical scan (and a sampled physical file scan) is refused.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Static review of 24badc84680733d7de6cb8e6be9f191903286b86: five new P2 defects are detailed inline. Fixes are needed for learned occurrence alignment, admin load-error reporting, SQL-mode-safe persistence, projection-stable outer-join expansion keys, and catalog-read locking.
| Critical checkpoint | Conclusion |
|---|---|
| Feature goal and correctness proof | Structural HBO identity, pinned management and learned reuse are implemented; the inline defects prevent an unqualified correctness conclusion. Unit and regression cases were inspected, not executed. |
| Scope and clarity | Large but coherent FE feature/refactor; physical rebuild edits support preservation of planner state. |
| Concurrency and lock scope/order | Planner memo caches stay on the planning thread. Admin requests and the background loader share pinnedLoadLock; internal SQL occurs under that lock. Same-FE stale removal checks replacement identity. Freshness admin reads omit the table lock needed against concurrent rollup DDL (inline); no additional lock cycle was established. |
| Lifecycle and initialization | Fingerprints are snapshotted before memo release; the loader retries and stops after loading. Admin access must distinguish pending/failed load from an empty cache (inline). No C++ static-initialization issue applies. |
| Configuration | New ratios are read dynamically; scan limits are session-scoped; pinned cache capacity is explicitly startup-only. Existing relation-table cache reconfiguration thread remains a duplicate fence. |
| Compatibility | Persisted fingerprints are opaque and the new table schema is checked. Historical keys/cluster fixtures do not prove compatibility with current structural hashes. No new FE-BE wire schema or function ABI is introduced. |
| Parallel paths | Logical/physical scans, embedded storage relations, final/partial aggregates, cost consumers and plan-copy paths were traced. Projection-sensitive outer expansion is new; already-reported physical selectors and learned-cost freshness are not reposted. |
| Special checks and errors | Unsupported/sample/temporary/selected external shapes fall back with diagnostics. Initial-load failure is incorrectly hidden from admin callers (inline); existing stale-delete write-error and expansion-fallback threads remain separate known issues. |
| Functional, negative and unit coverage | All 117 changed paths were covered across the reviewers; constructed plans, golden resources, parser/admin negatives, drift, injection and eight legacy renames were inspected. New scenarios described inline lack corresponding coverage. |
| Expected results | Golden builder/resource case catalogs align; the cluster corpus is historical. The already-reported equality-only expansion regression assertion remains known. No test result is claimed passing. |
| Observability | EXPLAIN includes fingerprint/type/mode/applied/skipped/expansion details and SHOW includes baseline/freshness. Failed initial loading currently gives misleading successful admin results (inline). |
| Persistence and failover | Pins write through to an internal UNIQUE KEY table and reload per FE. SQL-mode-dependent byte corruption is new (inline); prior deletion-visibility/cross-FE replacement threads were deduplicated. Distributed durability/failover was not exercised. |
| Data writes and atomicity | No committed user-data, transaction visibility or delete-bitmap execution path is changed. HBO metadata uses SQL transactions plus local locking; persistence failures and crash/reload behavior were reviewed statically. |
| FE-BE variables | Added settings and UDF identity accessors are FE-side; no missing new wire-field propagation path was identified. |
| Performance and memory | Scan-count and relation prefilters, bounded caches, group memoization and lazy rendering were inspected. The existing retained-relation-key issue remains known. No benchmark or native memory-accounting validation was performed. |
| Other issues and user focus | No additional user focus was supplied. Suspected VARIANT expansion and distinct-UDAF cases were dismissed using coercion/binder reachability; other repeated concerns were fenced by existing threads. |
Existing markdown threads and all 75 raw comments/replies were read, including older threads absent from the markdown window. Still-applicable identity/producer, learned population/version/skew, storage/tablet and persistence issues are already reported and are not duplicated. There are no supplied existing P0/P1 findings to carry forward.
Coverage limit: static source/diff/test inspection only; no builds, test execution, SQL execution, cluster startup or source modifications. The new findings describe code-proven triggers, not observed runtime reproductions.
Review converged after 2 round(s).
| continue; | ||
| } | ||
| Long currentTableId = currentTableIds.get(recordedScan.getRelationId()); | ||
| if (currentTableId != null && currentTableId != recordedScan.getTable().getId()) { |
There was a problem hiding this comment.
[P2] Require an occurrence match before borrowing scan predicates
Learn the full scan of a 1,000-row t(k=0..999) through SELECT t.k FROM (SELECT 1) d CROSS JOIN t, then plan Filter(k=0) -> Scan(t) as a single-table query. The dummy consumes a relation ID, so the historical t scan has ID 1 and the current scan ID 0. Their new scan fingerprints match, but this check accepts the missing historical ID. getFilterAdjustedInputTableStatistics then looks up predicates at ID 1, gets null and constructs an unfiltered input; FULL_MATCH accepts the old unfiltered measurement. computeFilter's scan-key fallback consequently applies output=1000 to the one-row filter, and freshness passes because the unchanged full scan population is 1000. Require a verified current occurrence binding, or reject this lookup. This uses one complete historical measurement, with no merge, missing storage input or data change, unlike the existing feedback threads.
There was a problem hiding this comment.
learned hbo 将重做, 不在这个 pr 范围
| * without triggering a load any more. | ||
| */ | ||
| public Map<String, PinnedHboStatistics> getAllPinnedPlanStatistics() { | ||
| ensurePinnedLoaded(); |
There was a problem hiding this comment.
[P2] Surface initial-load failures to statistics admin commands
After an FE restart with persisted pins, make the initial internal-table SELECT fail, for example while its backend or cloud compute group is unavailable. ensurePinnedLoaded returns normally on loadAll()==null and throughout its retry backoff; this method then presents the empty/partial cache as the complete set. HBO SHOW reports no stored entries and HBO DELETE STALE STATISTICS acknowledges success without inspecting or deleting persisted stale entries. The daemon later loads those untouched entries. Admin callers need an explicit load-success result and an error while the required load is incomplete. This differs from the existing per-entry DELETE-failure thread: no DELETE is attempted here and its failed-entry counter stays zero.
There was a problem hiding this comment.
Fixed: ensurePinnedLoaded now reports whether the required load completed; the admin entry point (getAllPinnedPlanStatistics) raises an error while it is incomplete instead of presenting a partial cache.
| + StatisticsUtil.escapeSQL((literalMode == null | ||
| ? HboPlanStatisticsManager.LiteralMode.NO_LITERAL : literalMode) | ||
| .name().toLowerCase(java.util.Locale.ROOT)) | ||
| + "', '" + StatisticsUtil.escapeSQL(struct) + "', " |
There was a problem hiding this comment.
[P2] Quote persisted structures for the internal session's SQL mode
With global sql_mode including NO_BACKSLASH_ESCAPES, pin an advertised filter on a supported quoted table named t\q. HboScanDescriptor leaves that backslash in the scan token, but StatisticsUtil.escapeSQL(struct) doubles it. execUpdate creates an internal session from the global defaults without normalizing SQL mode, so the INSERT stores two literal backslashes while SET acknowledges the correct one-backslash entry in memory. After restart, loading that row registers the doubled table name as its relation key; the identical live query uses the original name and is rejected by mayHavePinnedEntryForRelations before fingerprint lookup. Persist the structure without changing its bytes by using quoting consistent with the internal parsing session, or set an explicit compatible SQL mode for this write, and cover the persist/reload round trip.
There was a problem hiding this comment.
Fixed: the internal session of execUpdate clears NO_BACKSLASH_ESCAPES, so the values written with escapeSQL are parsed the way they were produced and a structure with a backslash keeps its bytes across a restart.
| /** The columns one side of an outer join produces, sorted: which side a relation is on is identity. */ | ||
| private static String sideOperands(Plan side) { | ||
| Set<String> identities = new TreeSet<>(); | ||
| for (Slot slot : side.getOutput()) { |
There was a problem hiding this comment.
[P2] Keep outer-join expansion keys stable under column pruning
Compare LEFT_JOIN(t.k=u.k, Scan(t(k,v)), Scan(u(k,w))) for SELECT * with Project(t.k,u.k) -> LEFT_JOIN(t.k=u.k, Project(k)->Scan(t), Project(k)->Scan(u)) for a key-only selection. ColumnPruning inserts the narrower child projects and the project-elimination passes retain them. This loop therefore puts t.k,t.v|u.k,u.w in the first key's sides field and only t.k|u.k in the second. The join reads the same rows under the same equality condition, yet an expansion copied from the first EXPLAIN silently misses the second because applyPinnedJoinExpansion only looks up that fingerprint. Bind the equality operands to stable input occurrences without including unrelated output columns. This is distinct from the existing outer-self-join collision: the equality population is identical here.
There was a problem hiding this comment.
Fixed: an outer join now orders each equality pair by side (left child operand first) and writes no output columns, so column pruning cannot change the key. A pair whose two operands come from one child, or name the same column, is refused (no key); the sides/sideOperands output-column scan is gone. Added paired tests for the side ordering, both refusal cases, and a key-only project below each child.
| liveTexts.add(recorded.getTable() + ":-"); | ||
| continue; | ||
| } | ||
| if (table.getIndexIdList().size() > 1 && !recorded.isPartitionSelectionComplete()) { |
There was a problem hiding this comment.
[P2] Snapshot catalog metadata under the table read lock
Run HBO SHOW or DELETE STALE for a pinned table while another session drops a rollup/materialized index. Neither admin caller takes the table read lock before entering of(). This getIndexIdList() calls getIndexIdToMeta(false), which copies a filtered view of OlapTable's plain HashMap. MaterializedViewHandler.processDropMaterializedView concurrently removes from that map through deleteIndexInfo under the table write lock; rollup-job publication also inserts under that lock. The unlocked copy can therefore throw ConcurrentModificationException and abort the admin command. Capture the needed local index/partition metadata under the matching read lock, releasing it before the cloud visible-version RPC, and add a coordinated reader/DDL case.
There was a problem hiding this comment.
Fixed: the index metadata of a pinned entry is now captured under the table read lock (released before the visible-version rpc), so a concurrent rollup/mv drop cannot abort HBO SHOW / DELETE STALE.
… its columns
### What problem does this solve?
Problem Summary: the key of an outer join expansion appended the columns each side produces
(`sides:[t.k,t.v|u.k,u.w]`) to say which relation is on the preserved side. Column pruning pushes a
key-only project below the join and the project elimination passes keep it, so the same join on the same
equality read `sides:[t.k,t.v|u.k,u.w]` with `SELECT *` and `sides:[t.k|u.k]` for a key-only selection:
the equality population is identical, but a factor copied from one EXPLAIN missed the other.
An outer join now orders the operands of each equality by side - the operand of the left child is written
first - which names the preserved side without any output column, so column pruning cannot change the key
anymore. A pair which cannot name a side has no key: both operands from one child (the mirror plan applies
the condition inside the other child) and a pair whose operands are the same column (the two occurrences
are interchangeable in the key although only one of them is preserved) are refused, as the factor of such
a join would describe an orientation it cannot tell apart. The `sides` field, `sideOperands` and the output
column scan are gone.
### Release note
An hbo join expansion entry of an outer join is keyed by the join type and the side ordered equality
pairs: it no longer changes under column pruning, and an outer join whose equality pair reads one child
only has no key.
### Check List (For Author)
- Test: Unit Test
- Paired tests pin the side ordered keys, the two refusal cases, and that a narrow (key-only) project
below each child keeps the key; 205 cases of the stats package plus the HBO command suite pass.
- Behavior changed: Yes - the outer join expansion key loses its output columns and refuses a pair
without a side.
- Does this need documentation: No
…ead lock
### What problem does this solve?
Problem Summary: the freshness comparison of a pinned entry called `table.getIndexIdList()` to know
whether the table has a rollup, without the table read lock. The list is a filtered copy of OlapTable's
plain index hash map, which a concurrent drop of a materialized view rewrites under the write lock, so an
HBO SHOW or DELETE STALE could throw ConcurrentModificationException and abort.
The index count is captured under the table read lock, which is released before the visible version rpc.
### Release note
HBO SHOW / HBO DELETE STALE no longer fail when a rollup or materialized view is dropped concurrently.
### Check List (For Author)
- Test: Unit Test
- 205 cases of the stats package plus the HBO command suite pass; a coordinated reader/DDL case is a
follow-up.
- Behavior changed: No
- Does this need documentation: No
… not loaded
### What problem does this solve?
Problem Summary: after a restart the pinned entries are loaded from the internal table. When that load
fails (the backend or the cloud compute group is unavailable), `ensurePinnedLoaded` returned normally,
so the admin caller presented the empty or partial cache as the complete set: HBO SHOW reported no stored
entries and HBO DELETE STALE acknowledged success without inspecting or deleting the persisted stale ones,
which the loader then installed later.
`ensurePinnedLoaded` now reports whether the required load completed, and `getAllPinnedPlanStatistics`
(the admin entry point) raises an error while it has not; the background loader keeps its retry loop.
### Release note
HBO SHOW and HBO DELETE STALE report an error instead of an empty result while the pinned statistics of
the FE could not be loaded.
### Check List (For Author)
- Test: Unit Test
- 205 cases of the stats package plus the HBO command suite pass; a load failure case needs the
internal table fixture and is a follow-up.
- Behavior changed: Yes - an incomplete initial load is an error for the admin statements.
- Does this need documentation: No
### What problem does this solve?
Problem Summary: the pinned entries are persisted with `StatisticsUtil.escapeSQL`, which doubles a
backslash assuming the standard sql mode, but the internal session of `execUpdate` inherited the global
default: with `NO_BACKSLASH_ESCAPES` a table name `t\q` was stored with two literal backslashes while the
in-memory entry kept one. After a restart the doubled name was registered as the relation key of that row,
and the identical live query was rejected before the fingerprint lookup.
The internal session now clears `NO_BACKSLASH_ESCAPES`, so both the write and every other internal
statement read the escaped values the way `escapeSQL` produces them.
### Release note
A pinned hbo entry whose structure contains a backslash (a quoted identifier, for example) keeps working
after an FE restart with NO_BACKSLASH_ESCAPES in the global sql_mode.
### Check List (For Author)
- Test: Unit Test
- 205 cases of the stats package plus the HBO command suite pass; a restart case with a backslash
identifier needs the persistence fixture and is a follow-up.
- Behavior changed: Yes - the internal session sql mode no longer inherits NO_BACKSLASH_ESCAPES.
- Does this need documentation: No
|
/review |
There was a problem hiding this comment.
Reviewed exact head d299294 using the supplied 117-file aggregate patch. One new P2 finding: a from_unixtime predicate can reuse an exact HBO pin across different session time zones. The implementation therefore still needs correction; this is not an approval.
Existing threads were independently rechecked against this head and used as duplicate fences, including the older raw comment archive. Known concerns remain in expression/producer identity, inner equality incidence, learned input alignment/populations/skew freshness, cross-FE replacement/deletion visibility, stale-cleanup error reporting, and the partially addressed index/partition snapshot. They are not reposted. No existing P0/P1 comment applies, so existing_blocking_comment_ids is empty. The failed-initial-load and SQL-mode fixes, pruning-stable outer expansion keys, sampled/temporary scan refusals, counted qualifier/subpath encoding, DISTINCT naming and commutative-key sorting are present.
Critical-checkpoint conclusions:
- Goal and correctness: the structural-key, pinned-statistics and admin/EXPLAIN flow is implemented and has targeted tests, but the new time-zone defect and existing concerns prevent full correctness. No storage-engine visibility or delete-bitmap behavior is changed.
- Scope and reuse: this is a large, coherent HBO feature; it reuses internal SQL execution, session contexts and Caffeine caches. Physical rebuild changes support retaining memo identity.
- Concurrency: independent admin/planner sessions, the loader daemon and profile workers share caches. Local monitors, concurrent containers, volatile state and the profile child lock cover their intended scopes; cross-FE deletion and incomplete metadata snapshots remain existing issues. Planning does not initiate persistence loads.
- Lifecycle: group descriptions stay planner-thread scoped; publication snapshots fingerprints before memo release. Loader retry/start and context restoration were traced. Existing relation-index retention concerns remain.
- Configuration: ratio/persistence settings are read dynamically; the pinned-cache size is documented as startup-only. The missing relation-table cache reconfiguration is already reported.
- Compatibility: structural keys replace legacy keys, and incompatible internal-table schemas are explicitly rejected. No new wire format or FE-BE field is introduced; restart/mixed-FE behavior has the existing persistence limits.
- Parallel paths: logical/physical, storage wrappers, filter/join/aggregate, exact/agnostic, expansion/learned/skew, and cloud/local version paths were inspected. Known feedback and physical TABLET gaps remain covered by existing threads.
- Conditions and errors: sample, temporary-partition, delimiter and unsupported-shape refusals have concrete reasons. Startup-load failures now reach admin callers; stale-cleanup per-entry failure reporting remains an existing issue.
- Test coverage: unit/golden/regression cases cover identities, counters, guards, local races, injection and admin SQL. They lack proof of real restart/multi-FE and coordinated metadata races; the new VARCHAR-valued time-zone case is absent.
- Expected results: output fixtures and assertions were inspected. The equality-only expansion regression's residual-literal assertion is still covered by existing comment 4355881610. Test execution and generated-result provenance were not verified.
- Observability: EXPLAIN keys/used/skipped states, SHOW details and failure logs expose the main decision paths; no additional distinct observability defect was established.
- Persistence/failover: internal-table DDL/DML use existing journal/transaction machinery. One-time FE-local loads, shared-row replacement and publication visibility retain the already-reported limitations.
- Writes/atomicity/crashes: local SET/DELETE/load phases are serialized and failed direct writes retain the active entry. This does not establish cross-FE compare-and-delete or visibility guarantees; those remain existing findings.
- FE-BE variables: none added; runtime feedback reads existing counters. No BE ownership or protocol change requires separate memory-tracker validation.
- Performance: memoized descriptions, lazy text, relation prefilters and scan-count limits bound ordinary planning work. Existing retained-index/cache-bound issues were deduplicated; no additional substantiated performance finding remains.
- Other risks and user focus: the suspected VARIANT expansion collision is blocked by analyzer casts/origin checks; comma-table descriptors are explicitly refused. All investigated candidates have accepted, duplicate or evidence-backed dismissed dispositions. No extra user focus was supplied.
Validation and coverage limits: static inspection only, as required. No builds, tests, benchmarks, cluster execution or source changes were performed. These limits do not imply that the existing unresolved review threads are fixed. All verified new findings are included inline.
Review converged after 2 round(s).
| */ | ||
| private static final List<String> EPOCH_TIME_ZONE_FUNCTIONS = Arrays.asList( | ||
| "hour_from_unixtime", "minute_from_unixtime", "second_from_unixtime", | ||
| "microsecond_from_unixtime", "unix_timestamp"); |
There was a problem hiding this comment.
[P2] Include from_unixtime in the time-zone-sensitive key
For Filter(Like(FromUnixtime(t.ts),'1970-01-01 00:%')) -> Scan(t) with BIGINT ts=0..3599, the predicate keeps 3600 rows in +00:00 and zero in +08:00. FromUnixtime has a BIGINT-to-VARCHAR signature, so neither its arguments nor its result activates the date-type check in usesSessionTimeZone; its name is also absent from this list. The structural fingerprints are consequently identical across the two sessions. A WITH_LITERAL pin copied in UTC then overrides the other session's filter estimate, because its unchanged scan baseline passes freshness. Include from_unixtime in the session-dependent identity and cover actual fingerprints for the same nonconstant predicate in both zones.
History-based optimizer statistics for the nereids planner
What problem does this solve?
The nereids optimizer estimates a plan from catalog statistics alone, so a query whose data distribution
or predicate selectivity the catalog cannot describe (a skewed join key, a selective range, a filter
whose matching rows are known from previous executions) is estimated from heuristics.
This PR adds a history-based optimizer (HBO):
of the node kinds, the normalized expressions, the scan identities and the children. Two literal modes
are keyed: the exact one and the constant agnostic one which folds the literals of a predicate. The key
is the sha256 of the structural identity of that tree (length-prefixed fields), not of its printed
form, so a table name, an alias or a literal which carries a character of the printed grammar cannot
stand for another field. The scan token records the table id, the relation alias, the index and the
selected partitions - the constant agnostic key is then only applied to the same data state.
statistics it recorded (rows read per scan) and against the current data state (visible version, row
count, partitions), so a stale or drifted entry is not applied. A published entry carries the
predicates of the scans it measured, the grouping definitions and the join conditions, so two queries
which differ in a predicate the key folds cannot share a count.
HBO SET/SHOW/DELETE STATISTICS(pinned entries),HBO SET LEARNED STATISTICS(tests and experts),
hbo_persist_pinned_to_internal_db(pinned entries survive a restart, withgeneration-bounded deletions), and
SET show_hbo_fingerprintprints the key and the struct of everynode in EXPLAIN together with the verdict of the pinned and learned entries.
Release note
plan; pinned statistics can be managed with the
HBO ... STATISTICSstatements, persisted to__internal_schema, andEXPLAINcan print the plan key together with the statistics which wereapplied.
Check List (For Author)
org.apache.doris.nereids.statspackage - struct-info golden corpus (76 plan cases, 51text cases, regenerated and asserted), scan identity, freshness, learned guards, pinned race,
expansion keys, input-table merge - plus the HBO command suites: 200 cases pass.
query_p0/hboto its baseline (19 suites; the 8legacy/*suites fail because the environment does not support them). The injection, drift,learned, persistence and display suites pass.
of a node with
SET show_hbo_fingerprint=true.need a manual.
Note
This PR is the squashed version of #67549 (one commit per branch tip instead of the incremental history),
opened with the same tree so the review can be done on a single change.
Known limitation: the join identity is bound to the relation aliases
Two queries which differ only by the names of their aliases get different keys:
Their fingerprints differ (
a0424a5c…against97991fdf…on a local cluster), because a join conditionis rendered with the alias qualifier of its slots and every scan token carries the alias of its relation
(
#ra:). A pinned or learned entry is therefore alias sensitive: renaming an alias invalidates theentries of the plan, and they have to be pinned again.
That is deliberate, and it is the safe direction: an alias is the only occurrence identity a plan
provides. A table can appear more than once, and the occurrences play different roles, which the join
conditions alone cannot tell apart - normalizing the operands to their table and column is not enough,
even for an inner join:
An occurrence index derived from the tree would be stable for neither of the two call sites (a group is
estimated with
GroupPlanchildren, while EXPLAIN prints the chosen plan), so it cannot replace thealias either.
Follow-up (not part of this PR): make the key alias insensitive with a canonical labelling of the join
graph - instance nodes labelled by their table, edges labelled by the columns and the operator, refined
deterministically into a canonical numbering. Automorphic instances then share a label, which is
harmless for a row-count estimate (swapping them only transposes the join output), while instances with
different roles (the two queries above) are distinguished. Until then, the alias stays part of the
identity.