github-actions[bot] commented on code in PR #69871:
URL: https://github.com/apache/doris/pull/69871#discussion_r4420152429
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1129,267 @@ private PhysicalPlan chooseBestPlanWithRequiredGroups(
return physicalPlan;
}
+ /**
+ * Compute the set of USE_MV hint qualifiers that successfully rewrote.
+ * Marks USE_MV hint parameters that did NOT rewrite-success as UnUsed.
+ */
+ private static Set<List<String>>
computeRequiredUseMvIdentifiers(StatementContext statementContext) {
+ Optional<UseMvHint> useMvHint =
statementContext.getUseMvHint("USE_MV");
+ if (!useMvHint.isPresent()) {
+ return ImmutableSet.of();
+ }
+ Set<List<String>> useMvQualifiers =
useMvHint.get().getUseMvTableColumnMap().keySet();
+ Set<List<String>> rewrittenSuccessSet =
statementContext.getMaterializationRewrittenSuccessSet();
+ Set<List<String>> required = new HashSet<>(useMvQualifiers);
+ required.retainAll(rewrittenSuccessSet);
+ // Mark USE_MV qualifiers that did not rewrite-success as UnUsed
+ Set<List<String>> notRewritten = new HashSet<>(useMvQualifiers);
+ notRewritten.removeAll(rewrittenSuccessSet);
+ for (List<String> qualifier : notRewritten) {
+ // Update hint map to mark as not-chosen (false) with failure
reason
+ useMvHint.get().getUseMvTableColumnMap().put(qualifier, false);
+ }
+ if (!notRewritten.isEmpty()) {
+ // If some USE_MV hints failed to rewrite, set status to UNUSED
with reason
+ useMvHint.get().setStatus(Hint.HintStatus.UNUSED);
+ useMvHint.get().setErrorMessage("MV(s) not rewrite-success after
CBO: " + notRewritten);
+ }
+ return required;
+ }
+
+ /**
+ * Extract the MV qualifier(s) from a scan plan if it matches a required
USE_MV identifier.
+ * Returns the set of MV qualifiers this plan represents.
+ */
+ private static Set<List<String>> getScanMvIdentifiers(Plan plan,
+ Set<List<String>> requiredUseMvIdentifiers) {
+ Set<List<String>> result = new HashSet<>();
+ if (plan instanceof PhysicalOlapScan) {
+ PhysicalOlapScan scan = (PhysicalOlapScan) plan;
+ OlapTable table = scan.getTable();
+ if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+ List<String> qualifier = new ArrayList<>();
+ qualifier.add(table.getDatabase().getCatalog().getName());
+ qualifier.add(table.getDatabase().getFullName());
+ qualifier.add(table.getName());
+
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ }
+ if (plan instanceof PhysicalCatalogRelation) {
+ PhysicalCatalogRelation rel = (PhysicalCatalogRelation) plan;
+ if (rel.getTable() instanceof MTMV) {
+ List<String> qualifier = rel.getTable().getFullQualifiers();
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ // Also check for sync MV via OlapScan (PhysicalOlapScan is a
PhysicalCatalogRelation)
+ if (rel instanceof PhysicalOlapScan) {
+ PhysicalOlapScan scan = (PhysicalOlapScan) rel;
+ OlapTable table = scan.getTable();
+ if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+ List<String> qualifier = new ArrayList<>();
+ qualifier.add(table.getDatabase().getCatalog().getName());
+ qualifier.add(table.getDatabase().getFullName());
+ qualifier.add(table.getName());
+
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Memoized bottom-up DFS: compute all MV identifiers reachable from this
group's subtree
+ * through physical GroupExpressions (including enforcers).
+ */
+ private static void computeReachableMvIdentifiers(Group group,
+ Map<Group, Set<List<String>>> cache, Set<List<String>>
requiredUseMvIdentifiers) {
+ if (cache.containsKey(group)) {
+ return;
+ }
+ Set<List<String>> reachable = new HashSet<>();
+ // Put in cache BEFORE recursing to break cycles
+ cache.put(group, reachable);
+
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ Set<List<String>> geReachable = getScanMvIdentifiers(ge.getPlan(),
requiredUseMvIdentifiers);
+ for (Group child : ge.children()) {
+ computeReachableMvIdentifiers(child, cache,
requiredUseMvIdentifiers);
+ geReachable.addAll(cache.get(child));
+ }
+ reachable.addAll(geReachable);
+ }
+ for (GroupExpression enforcer : group.getEnforcers().values()) {
+ Set<List<String>> enforcerReachable = new HashSet<>();
+ for (Group child : enforcer.children()) {
+ computeReachableMvIdentifiers(child, cache,
requiredUseMvIdentifiers);
+ enforcerReachable.addAll(cache.get(child));
+ }
+ reachable.addAll(enforcerReachable);
+ }
+ }
+
+ /**
+ * Choose the cheapest physical plan that contains all required MV
identifiers.
+ * At each group, picks the cheapest physical GroupExpression whose own +
children's
+ * reachable sets collectively cover all remaining required MV
identifiers, then
+ * distributes the remaining requirements among children and recurses.
+ */
+ private static PhysicalPlan chooseBestPlanWithRequiredMaterializations(
+ Group group, PhysicalProperties properties,
+ Set<List<String>> remaining, Map<Group, Set<List<String>>>
reachableCache,
+ CascadesContext cascadesContext) {
+ remaining = new HashSet<>(remaining);
+
+ // Remove MVs that this group's own scan expressions cover
+ Set<List<String>> ownMvs = new HashSet<>();
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ ownMvs.addAll(getScanMvIdentifiers(ge.getPlan(), remaining));
+ }
+ remaining.removeAll(ownMvs);
+
+ if (remaining.isEmpty()) {
+ return chooseBestPlan(group, properties, cascadesContext);
+ }
+
+ GroupExpression chosenGE = null;
+ double chosenCost = Double.MAX_VALUE;
+
+ // Check all physical GEs in this group
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ if (!ge.getLowestCostTable().containsKey(properties)) {
+ continue;
+ }
+ double geCost = ge.getCostByProperties(properties);
+ if (geCost >= chosenCost) {
+ continue;
+ }
+ Set<List<String>> childReachable = new HashSet<>();
+ for (Group child : ge.children()) {
+ Set<List<String>> cr = reachableCache.get(child);
+ if (cr != null) {
+ childReachable.addAll(cr);
+ }
+ }
+ if (childReachable.containsAll(remaining)) {
+ chosenGE = ge;
+ chosenCost = geCost;
+ }
+ }
+
+ // Also check enforcers
+ for (GroupExpression enforcer : group.getEnforcers().values()) {
+ if (!enforcer.getLowestCostTable().containsKey(properties)) {
+ continue;
+ }
+ double eCost = enforcer.getCostByProperties(properties);
+ if (eCost >= chosenCost) {
+ continue;
+ }
+ Set<List<String>> childReachable = new HashSet<>();
+ for (Group child : enforcer.children()) {
+ Set<List<String>> cr = reachableCache.get(child);
+ if (cr != null) {
+ childReachable.addAll(cr);
+ }
+ }
+ if (childReachable.containsAll(remaining)) {
+ chosenGE = enforcer;
+ chosenCost = eCost;
+ }
+ }
+
+ if (chosenGE == null) {
+ // The required materialization can not be produced with the
required properties, for example
+ // the hinted materialization is not costed for the required
distribution, so fall back to the
+ // cost based plan of this subtree instead of failing the query.
+ markUseMvNotChosen(cascadesContext.getStatementContext(),
remaining);
+ return chooseBestPlan(group, properties, cascadesContext);
+ }
+
+ // Handle enforcer marking (same as chooseBestPlan)
+ if ((chosenGE.getPlan() instanceof PhysicalDistribute
+ && group.getEnforcerSpecs().containsKey(
+ ((PhysicalDistribute<?>)
chosenGE.getPlan()).getDistributionSpec()))
+ || group.getEnforcers().containsKey(chosenGE)) {
+ group.addChosenEnforcerId(chosenGE.getId().asInt());
+ group.addChosenEnforcerProperties(properties);
+ } else {
+ group.setChosenProperties(properties);
+ group.setChosenGroupExpressionId(chosenGE.getId().asInt());
+ }
+
+ // Distribute remaining among children and recurse
+ List<PhysicalProperties> inputProps =
chosenGE.getInputPropertiesList(properties);
+ List<Plan> planChildren = Lists.newArrayList();
+ for (int i = 0; i < chosenGE.arity(); i++) {
+ Group child = chosenGE.child(i);
+ Set<List<String>> childReachable =
reachableCache.getOrDefault(child, new HashSet<>());
+ Set<List<String>> childRemaining = new HashSet<>(remaining);
+ childRemaining.retainAll(childReachable);
+ planChildren.add(chooseBestPlanWithRequiredMaterializations(
+ child, inputProps.get(i), childRemaining, reachableCache,
cascadesContext));
+ }
+
+ Plan plan = chosenGE.getPlan().withChildren(planChildren);
+ if (!(plan instanceof PhysicalPlan)) {
+ throw new AnalysisException("Result plan must be PhysicalPlan");
+ }
+ plan = plan.withGroupExpression(Optional.of(chosenGE));
+ PhysicalPlan physicalPlan = ((PhysicalPlan)
plan).withPhysicalPropertiesAndStats(
+ properties, group.getStatistics());
Review Comment:
[P1] Revalidate distribution contracts after forcing an MV child
The recursive child substitution can invalidate a join that was costed as
colocate. A concrete shape is colocated, unpartitioned HASH(k) tables A/B, with
A's duplicate-key prefix (dt,k), and a refreshed, non-colocated async m defined
as `SELECT k,dt,v FROM A WHERE dt>=0`, HASH(k), with a different bucket count.
For `SELECT /*+ USE_MV(m) */ A.k,A.v,B.k FROM A JOIN B ON A.k=B.k WHERE
A.dt=1`, use NOT_IN_RBO and a finite `filter_cost_factor` larger than A's
estimated scan cost plus 1. A's dt prefix matches, whereas m's leading k does
not, so Filter(A) stays cheaper despite m's zero scan cost. The filtered m can
rewrite Filter(dt=1), while predicate compensation rejects rewriting the
underlying unrestricted Scan(A).
The costed tree is `HashJoin(Filter(dt=1, Scan(A)), Scan(B))` with no
exchange. This selector replaces its left child with `Project(m.k AS A.k#1,
...) -> Filter(m.dt=1) -> Scan(m)`. Its NATURAL hash property compares equal to
the requested A property because `DistributionSpecHash.equals` ignores
table/index/partition identity; this copy then assigns A's metadata to that MV
project. `JoinUtils.shouldColocateJoin` and translation consequently still see
colocated A/B and omit the exchange needed for m/B. Metadata-only recomputation
also compares equal and does not repair it. Different bucket layouts can miss
matching rows or fail scheduling. Re-derive actual child outputs and re-run
property regulation/enforcement when forcing alternatives, rather than
retaining the original parent's distribution contract.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/InitMaterializationContextHook.java:
##########
@@ -131,15 +136,53 @@ protected void
doInitMaterializationContext(CascadesContext cascadesContext) {
private List<MaterializationContext>
getMvIdWithUseMvHint(List<MaterializationContext> mtmvCtxs,
UseMvHint
useMvHint) {
List<MaterializationContext> hintMTMVs = new ArrayList<>();
+ Set<List<String>> matchedQualifiers = new HashSet<>();
for (MaterializationContext mtmvCtx : mtmvCtxs) {
List<String> mvQualifier =
mtmvCtx.generateMaterializationIdentifier();
- if (useMvHint.getUseMvTableColumnMap().containsKey(mvQualifier)) {
+ Optional<List<String>> matchedParameter =
useMvHint.findUseMvParameter(mvQualifier);
+ if (matchedParameter.isPresent()) {
hintMTMVs.add(mtmvCtx);
+ matchedQualifiers.add(matchedParameter.get());
+ if (!matchedParameter.get().equals(mvQualifier)) {
+ // The sync MV is referenced by its bare name, e.g.
USE_MV(mv1) for table.mv1.
+ // Replace the parameter with the full identifier, so that
the later
+ // rewrite-success check and the hint status are computed
on the same qualifier.
+
useMvHint.getUseMvTableColumnMap().remove(matchedParameter.get());
Review Comment:
[P2] Preserve exact async references during sync-name normalization
A sync index and an async MV can have the same name because sync names are
table-local. Suppose t(k,v) has sync `m AS SELECT k AS mk FROM t`, while
refreshed async m contains `SELECT k,v FROM t`. For `SELECT /*+
USE_MV(internal.db.m) */ k,v FROM t` (or the bare-name form), the input is
`Project(k,v) -> Scan(t)` and only async m can answer it.
`doInitMaterializationContext` filters sync contexts first.
`findUseMvParameter` now matches the three-segment async identifier to sync
t.m, and this removal replaces it with [catalog,db,t,m] before query
compatibility is checked. The later async-context filter therefore excludes the
requested async m, while sync m cannot supply v, so the valid rewrite is lost.
This also breaks the previously unambiguous fully qualified async form.
Preserve original aliases or resolve exact async matches before consuming a
three-segment parameter as a sync bare name.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1129,267 @@ private PhysicalPlan chooseBestPlanWithRequiredGroups(
return physicalPlan;
}
+ /**
+ * Compute the set of USE_MV hint qualifiers that successfully rewrote.
+ * Marks USE_MV hint parameters that did NOT rewrite-success as UnUsed.
+ */
+ private static Set<List<String>>
computeRequiredUseMvIdentifiers(StatementContext statementContext) {
+ Optional<UseMvHint> useMvHint =
statementContext.getUseMvHint("USE_MV");
+ if (!useMvHint.isPresent()) {
+ return ImmutableSet.of();
+ }
+ Set<List<String>> useMvQualifiers =
useMvHint.get().getUseMvTableColumnMap().keySet();
+ Set<List<String>> rewrittenSuccessSet =
statementContext.getMaterializationRewrittenSuccessSet();
+ Set<List<String>> required = new HashSet<>(useMvQualifiers);
+ required.retainAll(rewrittenSuccessSet);
+ // Mark USE_MV qualifiers that did not rewrite-success as UnUsed
+ Set<List<String>> notRewritten = new HashSet<>(useMvQualifiers);
+ notRewritten.removeAll(rewrittenSuccessSet);
+ for (List<String> qualifier : notRewritten) {
+ // Update hint map to mark as not-chosen (false) with failure
reason
+ useMvHint.get().getUseMvTableColumnMap().put(qualifier, false);
+ }
+ if (!notRewritten.isEmpty()) {
+ // If some USE_MV hints failed to rewrite, set status to UNUSED
with reason
+ useMvHint.get().setStatus(Hint.HintStatus.UNUSED);
+ useMvHint.get().setErrorMessage("MV(s) not rewrite-success after
CBO: " + notRewritten);
+ }
+ return required;
+ }
+
+ /**
+ * Extract the MV qualifier(s) from a scan plan if it matches a required
USE_MV identifier.
+ * Returns the set of MV qualifiers this plan represents.
+ */
+ private static Set<List<String>> getScanMvIdentifiers(Plan plan,
+ Set<List<String>> requiredUseMvIdentifiers) {
+ Set<List<String>> result = new HashSet<>();
+ if (plan instanceof PhysicalOlapScan) {
+ PhysicalOlapScan scan = (PhysicalOlapScan) plan;
+ OlapTable table = scan.getTable();
+ if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+ List<String> qualifier = new ArrayList<>();
+ qualifier.add(table.getDatabase().getCatalog().getName());
+ qualifier.add(table.getDatabase().getFullName());
+ qualifier.add(table.getName());
+
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ }
+ if (plan instanceof PhysicalCatalogRelation) {
+ PhysicalCatalogRelation rel = (PhysicalCatalogRelation) plan;
+ if (rel.getTable() instanceof MTMV) {
+ List<String> qualifier = rel.getTable().getFullQualifiers();
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ // Also check for sync MV via OlapScan (PhysicalOlapScan is a
PhysicalCatalogRelation)
+ if (rel instanceof PhysicalOlapScan) {
+ PhysicalOlapScan scan = (PhysicalOlapScan) rel;
+ OlapTable table = scan.getTable();
+ if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+ List<String> qualifier = new ArrayList<>();
+ qualifier.add(table.getDatabase().getCatalog().getName());
+ qualifier.add(table.getDatabase().getFullName());
+ qualifier.add(table.getName());
+
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+ if (requiredUseMvIdentifiers.contains(qualifier)) {
+ result.add(qualifier);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Memoized bottom-up DFS: compute all MV identifiers reachable from this
group's subtree
+ * through physical GroupExpressions (including enforcers).
+ */
+ private static void computeReachableMvIdentifiers(Group group,
+ Map<Group, Set<List<String>>> cache, Set<List<String>>
requiredUseMvIdentifiers) {
+ if (cache.containsKey(group)) {
+ return;
+ }
+ Set<List<String>> reachable = new HashSet<>();
+ // Put in cache BEFORE recursing to break cycles
+ cache.put(group, reachable);
+
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ Set<List<String>> geReachable = getScanMvIdentifiers(ge.getPlan(),
requiredUseMvIdentifiers);
+ for (Group child : ge.children()) {
+ computeReachableMvIdentifiers(child, cache,
requiredUseMvIdentifiers);
+ geReachable.addAll(cache.get(child));
+ }
+ reachable.addAll(geReachable);
+ }
+ for (GroupExpression enforcer : group.getEnforcers().values()) {
+ Set<List<String>> enforcerReachable = new HashSet<>();
+ for (Group child : enforcer.children()) {
+ computeReachableMvIdentifiers(child, cache,
requiredUseMvIdentifiers);
+ enforcerReachable.addAll(cache.get(child));
+ }
+ reachable.addAll(enforcerReachable);
+ }
+ }
+
+ /**
+ * Choose the cheapest physical plan that contains all required MV
identifiers.
+ * At each group, picks the cheapest physical GroupExpression whose own +
children's
+ * reachable sets collectively cover all remaining required MV
identifiers, then
+ * distributes the remaining requirements among children and recurses.
+ */
+ private static PhysicalPlan chooseBestPlanWithRequiredMaterializations(
+ Group group, PhysicalProperties properties,
+ Set<List<String>> remaining, Map<Group, Set<List<String>>>
reachableCache,
+ CascadesContext cascadesContext) {
+ remaining = new HashSet<>(remaining);
+
+ // Remove MVs that this group's own scan expressions cover
+ Set<List<String>> ownMvs = new HashSet<>();
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ ownMvs.addAll(getScanMvIdentifiers(ge.getPlan(), remaining));
+ }
+ remaining.removeAll(ownMvs);
+
+ if (remaining.isEmpty()) {
+ return chooseBestPlan(group, properties, cascadesContext);
+ }
+
+ GroupExpression chosenGE = null;
+ double chosenCost = Double.MAX_VALUE;
+
+ // Check all physical GEs in this group
+ for (GroupExpression ge : group.getPhysicalExpressions()) {
+ if (!ge.getLowestCostTable().containsKey(properties)) {
+ continue;
+ }
+ double geCost = ge.getCostByProperties(properties);
+ if (geCost >= chosenCost) {
+ continue;
+ }
+ Set<List<String>> childReachable = new HashSet<>();
+ for (Group child : ge.children()) {
+ Set<List<String>> cr = reachableCache.get(child);
+ if (cr != null) {
+ childReachable.addAll(cr);
+ }
+ }
+ if (childReachable.containsAll(remaining)) {
+ chosenGE = ge;
+ chosenCost = geCost;
+ }
+ }
+
+ // Also check enforcers
+ for (GroupExpression enforcer : group.getEnforcers().values()) {
+ if (!enforcer.getLowestCostTable().containsKey(properties)) {
+ continue;
+ }
+ double eCost = enforcer.getCostByProperties(properties);
+ if (eCost >= chosenCost) {
+ continue;
+ }
+ Set<List<String>> childReachable = new HashSet<>();
+ for (Group child : enforcer.children()) {
+ Set<List<String>> cr = reachableCache.get(child);
+ if (cr != null) {
+ childReachable.addAll(cr);
+ }
+ }
+ if (childReachable.containsAll(remaining)) {
+ chosenGE = enforcer;
+ chosenCost = eCost;
+ }
+ }
+
+ if (chosenGE == null) {
+ // The required materialization can not be produced with the
required properties, for example
+ // the hinted materialization is not costed for the required
distribution, so fall back to the
+ // cost based plan of this subtree instead of failing the query.
+ markUseMvNotChosen(cascadesContext.getStatementContext(),
remaining);
+ return chooseBestPlan(group, properties, cascadesContext);
+ }
+
+ // Handle enforcer marking (same as chooseBestPlan)
+ if ((chosenGE.getPlan() instanceof PhysicalDistribute
+ && group.getEnforcerSpecs().containsKey(
+ ((PhysicalDistribute<?>)
chosenGE.getPlan()).getDistributionSpec()))
+ || group.getEnforcers().containsKey(chosenGE)) {
+ group.addChosenEnforcerId(chosenGE.getId().asInt());
+ group.addChosenEnforcerProperties(properties);
+ } else {
+ group.setChosenProperties(properties);
+ group.setChosenGroupExpressionId(chosenGE.getId().asInt());
+ }
+
+ // Distribute remaining among children and recurse
+ List<PhysicalProperties> inputProps =
chosenGE.getInputPropertiesList(properties);
+ List<Plan> planChildren = Lists.newArrayList();
+ for (int i = 0; i < chosenGE.arity(); i++) {
+ Group child = chosenGE.child(i);
+ Set<List<String>> childReachable =
reachableCache.getOrDefault(child, new HashSet<>());
+ Set<List<String>> childRemaining = new HashSet<>(remaining);
+ childRemaining.retainAll(childReachable);
+ planChildren.add(chooseBestPlanWithRequiredMaterializations(
+ child, inputProps.get(i), childRemaining, reachableCache,
cascadesContext));
+ }
+
+ Plan plan = chosenGE.getPlan().withChildren(planChildren);
+ if (!(plan instanceof PhysicalPlan)) {
+ throw new AnalysisException("Result plan must be PhysicalPlan");
+ }
+ plan = plan.withGroupExpression(Optional.of(chosenGE));
+ PhysicalPlan physicalPlan = ((PhysicalPlan)
plan).withPhysicalPropertiesAndStats(
+ properties, group.getStatistics());
+ return physicalPlan;
+ }
+
+ /**
+ * After final physical plan is chosen, validate USE_MV hint status
+ * by checking which required MVs are actually in the chosen plan.
+ */
+ public static void finalizeUseMvHintStatus(StatementContext
statementContext,
+ List<MaterializationContext> materializationContexts, PhysicalPlan
physicalPlan) {
+ Optional<UseMvHint> useMvHint =
statementContext.getUseMvHint("USE_MV");
+ if (!useMvHint.isPresent()) {
+ return;
+ }
+ // Do not override SYNTAX_ERROR status set during init phase
+ if (useMvHint.get().isSyntaxError()) {
+ return;
+ }
+ // Collect actually chosen MV qualifiers from the physical plan
+ Set<List<String>> chosenQualifiers =
MaterializationContext.getChosenMvsQualifiers(
Review Comment:
[P2] Count selected sync indexes independently of rewrite success
For a projection sync index `m AS SELECT k AS mk FROM t`, `EXPLAIN SELECT
/*+ USE_MV(t.m) */ mk FROM t INDEX m` with NOT_IN_RBO produces `Project(mk) ->
PhysicalOlapScan(t, selectedIndexId=m)`. `BindRelation` binds that index
directly. The new pre-record hook puts its identifier in the statement
rewrite-success set, but the context's `success` remains false: the MV rule's
precheck rejects rewriting a scan already on a non-base index.
`getChosenMvsQualifiers` filters contexts through
`MaterializationContext::isSuccess`, so this call returns no chosen m even
though the final scan uses it; the finalizer then reports UnUsed and marks its
map entry false. The deleted scan-cost branch reported Used for the same
supported direct-index query. Final actual-selection status should derive
identifiers from selected scans, or include applicable sync contexts without
requiring a separate successful CBO rewrite.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]