github-actions[bot] commented on code in PR #69871:
URL: https://github.com/apache/doris/pull/69871#discussion_r4421163854
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -883,6 +919,112 @@ public static PhysicalPlan chooseBestPlan(Group
rootGroup, PhysicalProperties ph
}
}
+ /**
+ * Choose the best physical plan considering USE_MV hint as a hard
constraint.
+ * If USE_MV hint is present with rewrite-success MVs, the returned plan
must
+ * contain those MVs. Otherwise, falls back to standard cost-based
selection.
+ *
+ * @param rootGroup the root memo group
+ * @param physicalProperties required physical properties
+ * @param cascadesContext the cascades context
+ * @return physical plan respecting USE_MV hard constraint
+ */
+ public static PhysicalPlan chooseBestPlanConsideringUseMv(Group rootGroup,
+ PhysicalProperties physicalProperties, CascadesContext
cascadesContext)
+ throws AnalysisException {
+ StatementContext stmtCtx = cascadesContext.getStatementContext();
+ Set<List<String>> requiredUseMvIdentifiers =
computeRequiredUseMvIdentifiers(stmtCtx);
+ if (requiredUseMvIdentifiers.isEmpty()) {
+ return chooseBestPlan(rootGroup, physicalProperties,
cascadesContext);
+ }
+ Map<Group, Set<List<String>>> mvReachableCache = new HashMap<>();
+ computeReachableMvIdentifiers(rootGroup, mvReachableCache,
requiredUseMvIdentifiers);
+ Set<List<String>> rootReachable = mvReachableCache.get(rootGroup);
+ // Drop the required MVs which the rewrite can not produce, and mark
them as UnUsed,
+ // USE_MV is best-effort and should not fail the query.
+ requiredUseMvIdentifiers = dropUnreachableUseMvIdentifiers(stmtCtx,
requiredUseMvIdentifiers,
+ rootReachable == null ? ImmutableSet.of() : rootReachable);
+ if (requiredUseMvIdentifiers.isEmpty()) {
+ return chooseBestPlan(rootGroup, physicalProperties,
cascadesContext);
+ }
+ return chooseBestPlanConsideringRequiredUseMv(rootGroup,
physicalProperties,
+ requiredUseMvIdentifiers, mvReachableCache, cascadesContext);
+ }
+
+ /**
+ * Choose the best plan which contains the required USE_MV
materializations.
+ * <p>
+ * Several hinted materializations may be alternatives of each other and
can not be chosen at the
+ * same time, so only the single required materialization is enforced. The
cost model already makes
+ * the hinted materializations the cheapest ones, so the cost based plan
is used for the others.
+ */
+ private static PhysicalPlan chooseBestPlanConsideringRequiredUseMv(Group
rootGroup,
+ PhysicalProperties physicalProperties, Set<List<String>>
requiredUseMvIdentifiers,
+ Map<Group, Set<List<String>>> mvReachableCache, CascadesContext
cascadesContext)
+ throws AnalysisException {
+ if (requiredUseMvIdentifiers.size() == 1) {
+ PhysicalPlan physicalPlan =
chooseBestPlanWithRequiredMaterializations(rootGroup,
+ physicalProperties, requiredUseMvIdentifiers,
mvReachableCache, cascadesContext);
+ if (providesProperties(physicalPlan.getPhysicalProperties(),
physicalProperties)) {
+ return physicalPlan;
+ }
+ // The forced plan does not provide the required output properties
anymore, for example a
+ // materialization replaced one side of a colocate join and both
sides are shuffled now.
+ PhysicalPlan enforced = addExchange(physicalPlan,
physicalProperties.getDistributionSpec());
+ if
(!enforced.getPhysicalProperties().getOrderSpec().satisfy(physicalProperties.getOrderSpec()))
{
+ enforced = addSort(enforced,
physicalProperties.getOrderSpec());
+ }
+ return enforced;
+ }
+ return chooseBestPlan(rootGroup, physicalProperties, cascadesContext);
Review Comment:
[P2] Enforce a reachable alternative for multiple-MV hints
This fallback can choose none of the hinted MVs even when both rewrites
succeed. For example, use `NOT_IN_RBO`, base `A(dt,k,v) DUPLICATE KEY(dt,k)`,
and refreshed m1/m2 each defined as `SELECT k,dt,v FROM A WHERE dt>=0`. For
`SELECT /*+ USE_MV(m1,m2) */ k,dt,v FROM A WHERE dt=1`, the memo contains
`Filter(dt=1) -> Scan(A)` and each valid `Project -> Filter(m.dt=1) ->
Scan(mi)` replacement.
Zero scan cost does not make the whole MV plan cheapest: A's dt prefix
avoids one `filter_cost_factor` that each MV's leading k does not. Set that
factor large enough to outweigh the finite base scan cost and the base
alternative remains cheaper. Both MV identifiers enter the required/reachable
sets, but size>1 reaches this unconditional `chooseBestPlan`, selecting A and
reporting both unused. Adding a valid alternative therefore disables the new
hard constraint. Select the cheapest plan containing at least one reachable
hinted alternative instead of dropping enforcement for the entire set.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1148,432 @@ 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));
+ }
+
+ // The children were chosen by the materialization reachability
instead of the cost, so the
+ // property contract of the costed group expression can be stale: a
child can be replaced by a
+ // materialization which is laid out by another relation. Re-derive
the actual children outputs
+ // and enforce the missing properties again, instead of keeping the
costed contract.
+ boolean contractHolds = childrenProvideProperties(inputProps,
planChildren);
+ if (!contractHolds) {
+ enforceChildrenProperties(inputProps, planChildren);
+ }
+
+ Plan plan = chosenGE.getPlan().withChildren(planChildren);
+ if (!(plan instanceof PhysicalPlan)) {
+ throw new AnalysisException("Result plan must be PhysicalPlan");
+ }
+ plan = plan.withGroupExpression(Optional.of(chosenGE));
+ PhysicalProperties outputProperties = contractHolds
+ ? chosenGE.getOutputProperties(properties)
+ : deriveOutputProperties(plan, planChildren, cascadesContext);
+ return ((PhysicalPlan)
plan).withPhysicalPropertiesAndStats(outputProperties, group.getStatistics());
+ }
+
+ /**
+ * Check whether the actual physical properties provide the required ones.
+ * <p>
+ * {@link DistributionSpecHash#satisfy} and {@link
DistributionSpecHash#equals} compare the shuffle
+ * type and the shuffle columns only, they ignore the relation, the index
and the partitions. A
+ * materialization which replaces a relation is laid out by its own table,
so it compares equal to
+ * the distribution of the replaced relation even though the two are not
co-located. The layout
+ * identity must be compared explicitly when the property regulation of
the cost based selection is
+ * by-passed.
+ */
+ private static boolean providesProperties(PhysicalProperties actual,
PhysicalProperties required) {
+ return providesDistribution(actual.getDistributionSpec(),
required.getDistributionSpec())
+ && actual.getOrderSpec().satisfy(required.getOrderSpec());
+ }
+
+ private static boolean providesDistribution(DistributionSpec actual,
DistributionSpec required) {
+ if (!actual.satisfy(required)) {
+ return false;
+ }
+ if (!(required instanceof DistributionSpecHash) || !(actual instanceof
DistributionSpecHash)) {
+ return true;
+ }
+ DistributionSpecHash requiredHash = (DistributionSpecHash) required;
+ ShuffleType requiredShuffleType = requiredHash.getShuffleType();
+ if (requiredShuffleType != ShuffleType.NATURAL && requiredShuffleType
!= ShuffleType.STORAGE_BUCKETED) {
+ // The required distribution does not identify a storage layout.
+ return true;
+ }
+ DistributionSpecHash actualHash = (DistributionSpecHash) actual;
+ return actualHash.getTableId() == requiredHash.getTableId()
+ && actualHash.getSelectedIndexId() ==
requiredHash.getSelectedIndexId()
+ &&
actualHash.getPartitionIds().equals(requiredHash.getPartitionIds());
+ }
+
+ /**
+ * Check whether the actual distribution delivers the shuffle columns of
the required one. Unlike
+ * {@link #providesDistribution}, a distribution with another storage
layout is accepted, because a
+ * single child only propagates its distribution to the group expression,
so replacing it with a
+ * materialization does not need a data movement as long as the shuffle
columns keep matching.
+ */
+ private static boolean providesShuffleColumns(DistributionSpec actual,
DistributionSpec required) {
+ if (!(required instanceof DistributionSpecHash) || !(actual instanceof
DistributionSpecHash)) {
+ return actual.satisfy(required);
+ }
+ return actual.satisfy(((DistributionSpecHash)
required).withShuffleType(ShuffleType.REQUIRE));
+ }
+
+ /**
+ * Check whether every forced child provides the properties the memo
costed the group expression
+ * with.
+ */
+ private static boolean childrenProvideProperties(List<PhysicalProperties>
inputProps,
+ List<Plan> planChildren) {
+ for (int i = 0; i < planChildren.size(); i++) {
+ if
(!providesProperties(getPhysicalProperties(planChildren.get(i)),
inputProps.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Enforce the input properties of the group expression on the forced
children plans, the same way
+ * {@link org.apache.doris.nereids.properties.ChildrenPropertiesRegulator}
does it during the cost
+ * based selection.
+ */
+ private static void enforceChildrenProperties(List<PhysicalProperties>
inputProps,
+ List<Plan> planChildren) {
+ boolean multiChild = planChildren.size() > 1;
+ for (int i = 0; i < planChildren.size(); i++) {
+ PhysicalProperties required = inputProps.get(i);
+ PhysicalProperties actual =
getPhysicalProperties(planChildren.get(i));
+ if (!providesProperties(actual, required)) {
+ // Every hash distributed child of a join or of a set
operation is realigned, not only
+ // the replaced one: the data layout pairing of the costed
plan does not hold anymore
+ // once one child of such a group expression is replaced by a
materialization.
+ if (multiChild && required.getDistributionSpec() instanceof
DistributionSpecHash) {
+ planChildren.set(i, addExchange(planChildren.get(i),
required.getDistributionSpec()));
+ } else if (!providesShuffleColumns(
+ actual.getDistributionSpec(),
required.getDistributionSpec())) {
+ planChildren.set(i, addExchange(planChildren.get(i),
required.getDistributionSpec()));
+ }
+ }
+ if
(!getPhysicalProperties(planChildren.get(i)).getOrderSpec().satisfy(required.getOrderSpec()))
{
+ planChildren.set(i, addSort(planChildren.get(i),
required.getOrderSpec()));
+ }
+ }
+ }
+
+ /**
+ * Add an exchange which delivers the required distribution. A hash
distribution without a shuffle
+ * type is delivered by a hash shuffle, the same way
+ * {@link
org.apache.doris.nereids.properties.EnforceMissingPropertiesHelper} does it.
+ */
+ private static PhysicalPlan addExchange(Plan child, DistributionSpec
required) {
+ DistributionSpec exchangeDistribution = required instanceof
DistributionSpecHash
+ ? ((DistributionSpecHash)
required).withShuffleType(ShuffleType.EXECUTION_BUCKETED)
+ : required;
+ return new PhysicalDistribute<>(exchangeDistribution,
Optional.empty(), child.getLogicalProperties(),
+ new PhysicalProperties(exchangeDistribution),
child.getStats(), child);
+ }
+
+ private static PhysicalPlan addSort(Plan child, OrderSpec requiredOrder) {
+ SortPhase sortPhase = requiredOrder instanceof MustLocalSortOrderSpec
+ ? SortPhase.LOCAL_SORT : SortPhase.MERGE_SORT;
Review Comment:
[P2] Restore local ordering before constructing a merge sort
An ordinary `OrderSpec` does not imply that the child is already a local
sort. A reachable example is a twice-used materialized CTE containing
`row_number() OVER (PARTITION BY k ORDER BY v)`, with both consumers retaining
the row number. Use `NOT_IN_RBO`, `enable_cte_materialize=true`,
`inline_cte_referenced_threshold=1`, `cte_inline_mode=-1`, base `A(dt,k,v)
DUPLICATE KEY(dt,k) HASH(k)`, and refreshed `m AS SELECT k,dt,v FROM A WHERE
dt>=0 HASH(k)`. A sufficiently large finite `filter_cost_factor` keeps the base
filter cheaper because only A matches the dt prefix.
The reduced producer is `CTEProducer -> Window(k,v) -> LocalSort(k,v) ->
Filter(dt=1) -> Scan(A)`. Forcing m propagates its different storage identity
through the unary nodes. CTEAnchor records the original producer's actual
`NATURAL(A)+Order(k,v)` input, then `enforceChildrenProperties` treats the
anchor's two children as paired inputs and inserts an exchange above the
producer. That drops its order, so this helper builds `MERGE_SORT -> Exchange
-> CTEProducer -> Window(...)`.
`visitPhysicalCTEProducer` returns the window's fragment, whose root is
`AnalyticEvalNode` (or an upper projection's SelectNode).
`visitPhysicalQuickSort` casts the exchange's immediate child to `SortNode`, so
translation throws `ClassCastException`. Respect CTEAnchor's producer contract
and create a real local sort when restoring missing order; MERGE_SORT requires
an already sorted input.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1148,432 @@ 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));
Review Comment:
[P2] Rebuild enforcers using the hinted MV's available properties
Reusing a cheapest enforcer's input property can discard a valid single
hinted MV. Set `NOT_IN_RBO` and `enable_parallel_result_sink=false`; use base
`A(dt,k,v) DUPLICATE KEY(dt,k) HASH(k)` and refreshed `m AS SELECT k,dt,v FROM
A WHERE dt>=0 HASH(v)`. For `SELECT /*+ USE_MV(m) */ k,dt,v FROM A WHERE dt=1`,
choose a finite `filter_cost_factor` large enough that A's dt-prefix saving
keeps the base plan cheaper.
The ResultSink requires GATHER. Its shared gather enforcer retains the
cheapest input `NATURAL(k)` from the base plan; original MV expressions are
recorded under ANY/native `NATURAL(v)`, and higher-cost distribution
enforcement can be pruned by `CostAndEnforcerJob`. Reachability sees m through
the enforcer's self-group, but this recursion requests the retained
`NATURAL(k)`. The hinted Project fails the `containsKey(properties)` check, and
selection falls back to `Gather -> Filter -> Scan(A)`, reporting m unused.
`Gather -> Project -> Filter -> Scan(m)` would satisfy the same parent
contract. Select the hinted alternative on its available ANY/native property
and rebuild the exchange, rather than requiring it to reproduce the cheapest
base's storage hash.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -322,27 +338,47 @@ private Plan planWithoutLock(
LOG.info("{}\nMemo is null",
ConnectContext.get().getQueryIdentifier());
}
}
- Set<Integer> requiredGroupIds = cascadesContext.getConnectContext()
- .getSessionVariable().getRequiredGroupIds();
+
+ // Compute USE_MV hard constraint: USE_MV specified qualifiers that
rewrite-successfully
+ Set<List<String>> requiredUseMvIdentifiers =
computeRequiredUseMvIdentifiers(statementContext);
+ Map<Group, Set<List<String>>> mvReachableCache = new HashMap<>();
+ if (!requiredUseMvIdentifiers.isEmpty()) {
+ computeReachableMvIdentifiers(getRoot(), mvReachableCache,
requiredUseMvIdentifiers);
+ // Drop the required MVs which the rewrite can not produce, and
mark them as UnUsed,
+ // USE_MV is best-effort and should not fail the query.
+ Set<List<String>> rootReachable = mvReachableCache.get(getRoot());
+ requiredUseMvIdentifiers =
dropUnreachableUseMvIdentifiers(statementContext,
+ requiredUseMvIdentifiers,
+ rootReachable == null ? ImmutableSet.of() : rootReachable);
+ }
PhysicalPlan physicalPlan;
- if (!requiredGroupIds.isEmpty()) {
- Map<Group, Set<Integer>> reachableCache = new HashMap<>();
- computeReachableGroupIds(getRoot(), reachableCache);
- // Validate all required groups are reachable from root
- Set<Integer> rootReachable = reachableCache.get(getRoot());
- Set<Integer> unreachable = new HashSet<>(requiredGroupIds);
- unreachable.removeAll(rootReachable);
- if (!unreachable.isEmpty()) {
- throw new AnalysisException("Required group IDs not found in
memo: " + unreachable);
- }
- physicalPlan = chooseBestPlanWithRequiredGroups(
- getRoot(), requireProperties, requiredGroupIds,
reachableCache);
+ if (!requiredUseMvIdentifiers.isEmpty()) {
+ physicalPlan = chooseBestPlanConsideringRequiredUseMv(getRoot(),
requireProperties,
Review Comment:
[P3] Populate the plan cost on the USE_MV selection path
This branch bypasses `chooseNthPlan` and `chooseBestPlanWithRequiredGroups`,
the two instance methods that assign the planner's `cost` field. The new static
helpers never assign it, so `EXPLAIN OPTIMIZED PLAN` prints the default `cost =
0.0` for every reachable USE_MV selection. For example, with refreshed `m AS
SELECT k FROM t` and `NOT_IN_RBO`, `SELECT /*+ USE_MV(m) */ k+1 FROM t` selects
`Project(k+1) -> Scan(m)`; `visitPhysicalProject` charges a positive cost even
though the hinted scan costs zero. Preserve the selected plan's estimate,
including any synthesized enforcement, so the existing EXPLAIN cost remains
accurate.
--
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]