github-actions[bot] commented on code in PR #68648:
URL: https://github.com/apache/doris/pull/68648#discussion_r4360642848
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java:
##########
@@ -377,6 +383,23 @@ protected List<Plan> doRewrite(StructInfo queryStructInfo,
CascadesContext casca
}
boolean needRemoveMvPartitions =
!invalidPartitions.key().isEmpty();
boolean needBaseTableUnion =
!invalidPartitions.value().isEmpty();
+ // An uncovered base partition is read beside the MV
partitions that stay, and the two can
+ // answer the same aggregate group -- a merged LIST
component's MV partition holds a key the
+ // uncovered partition also carries. The union is below the
aggregate then, so the group is
+ // two rows rather than one. A group expression over the
partition column has the same
+ // effect even when the keys differ: date_trunc(d,'year') puts
every day of the year in one
+ // group. Answered from the base table instead of splitting
them.
+ boolean splitsAggregateGroup =
queryPlan.anyMatch(LogicalAggregate.class::isInstance)
+ && needBaseTableUnion
+ && (!uncoveredNarrowedBasePartitions.isEmpty()
+ || (!needRemoveMvPartitions &&
groupsByExpressionOverPctColumn(queryPlan, mtmv)));
Review Comment:
[P1] Keep computed-group protection when only some MV partitions are removed
Removing a stale partition does not imply that every MV partition is
removed. Extend the new `group_split_expr` fixture with a populated
`p_stale={(2037-01-01,3)}`, refresh the MV, then write `p_stale`. Query `d IN
('2020-01-01','2020-01-02','2037-01-01') GROUP BY date_trunc(d,'year')`. The
fresh merged component still holds Jan 2 with v=2; uncovered `p_old` holds Jan
1 with v=1, while the separate 2037 MV partition is stale.
`calcInvalidPartitions` removes only the 2037 partition and compensates
`{p_old,p_stale}`. Raw dates do not overlap, so
`uncoveredNarrowedBasePartitions` is empty; `needRemoveMvPartitions=true` then
disables this expression check. `canUnionRewrite` accepts the PCT slot inside
`date_trunc`, producing this reduced plan:
```text
UnionAll
Aggregate(year(d),sum(s)) -> Scan MV[fresh merged component]
Aggregate(year(d),sum(v)) -> Filter(query AND keys(old,stale)) -> Scan t
```
The two branches emit separate year-2020 rows with 2 and 1, instead of one
row with 3. Unlike #4356885832's fixed same-date case, this bypass needs
computed groups and a simultaneous stale removal. Check overlap against the MV
partitions that actually remain, even when another partition is removed, or
merge aggregate states above the union.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java:
##########
@@ -504,6 +528,42 @@ protected boolean canUnionRewrite(Plan queryPlan,
AsyncMaterializationContext co
return true;
}
+ /**
+ * Whether the query groups by an expression over a column the MV is
partitioned on, rather than by the
+ * column itself. Every value of such a group expression is a function of
several partition-column values
+ * -- {@code date_trunc(d,'year')} puts every day of the year in one group
-- so an uncovered base
+ * partition and a retained MV partition can answer the same group even
when their keys differ.
+ */
+ private static boolean groupsByExpressionOverPctColumn(Plan queryPlan,
MTMV mtmv) {
+ Optional<LogicalAggregate<Plan>> aggregate =
+ queryPlan.collectFirst(LogicalAggregate.class::isInstance);
+ if (!aggregate.isPresent()) {
+ return false;
+ }
+ List<BaseColInfo> pctInfos = mtmv.getMvPartitionInfo().getPctInfos();
+ List<? extends Expression> groupByExpressions =
ExpressionUtils.shuttleExpressionWithLineage(
+ aggregate.get().getGroupByExpressions(), queryPlan);
+ for (Expression expression : groupByExpressions) {
+ if (expression instanceof SlotReference) {
Review Comment:
[P1] Check each grouping set before treating slot grouping keys as disjoint
The normalized group-by list hides grouping sets that omit the partition
column. With the new `group_split_expr` fixture unchanged and fresh, query
`SELECT d,SUM(v) FROM group_split_expr_base WHERE d IN
('2020-01-01','2020-01-02') GROUP BY GROUPING SETS ((d),())`. The MV holds Jan
2 (v=2), and uncovered `p_old` holds Jan 1 (v=1). Their raw dates do not
overlap.
`NormalizeRepeat` builds aggregate keys `[d, grouping_id]` and preserves
`sourceRepeat=[[d],[]]`. Both keys remain `SlotReference`s through lineage, so
this loop skips them and returns false. `canUnionRewrite` accepts `d`, while
`aggregateRewriteByView` explicitly rewrites the repeat over the MV. The
reduced result is:
```text
UnionAll
Aggregate[sets(d),()] -> Repeat -> Scan MV // (Jan2,2), (NULL,2)
Aggregate[sets(d),()] -> Repeat -> Filter(keys(old)) -> Scan t
// (Jan1,1), (NULL,1)
```
There is no aggregate above this union, so it returns two grand totals
instead of `(NULL,3)`. This is separate from the stale-partition bypass: no
partition removal or computed group expression is needed. Inspect
`sourceRepeat`/each grouping set and reject compensation when a set can span
both branches, or combine its aggregate states correctly.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java:
##########
@@ -144,19 +191,198 @@ public static Pair<Map<BaseTableInfo, Set<String>>,
Map<BaseColInfo, Set<String>
if (allCompensateIsNull) {
return null;
}
+ // A base partition the query reads that no MV partition of this plan
is named from -- one the
+ // partition_sync_limit window left out, say -- is one the MV does not
answer for: a partition of the MV
+ // reads a table through the partitions it does name, and the mapping
has no entry for this one. Its
+ // rows have to come from the base table, and two things follow from
that, decided apart.
+ //
+ // The base branch of the union reads those partitions. A table the
mapping mentions not at all is
+ // uncovered in every partition the query uses, so it is read for the
query's own partitions there
+ // rather than skipped -- without this the plan would answer for that
table out of the MV alone.
+ //
+ // And the MV partitions this plan uses have to be taken out when they
hold rows of such a partition: a
+ // partition the window has since left out can have been materialized
in one of them by an earlier
+ // refresh, and the union would then count its rows twice. The refresh
snapshot names exactly the
+ // partitions a refresh read into each MV partition, so that is what
this is asked of. (It is not the
+ // keys: a list partition's default one holds the keys no other
partition of the table claims, so a
+ // partition can be read into an MV partition whose keys do not cover
it.)
+ Map<MTMVRelatedTableIf, Set<String>> uncoveredByTable =
Maps.newLinkedHashMap();
+ boolean uncoveredRecorded = false;
+ for (MTMVRelatedTableIf pctTable :
mtmv.getMvPartitionInfo().getPctTables()) {
+ Set<String> queryUsed =
queryUsedBaseTablePartitionMap.get(pctTable.getFullQualifiers());
+ if (CollectionUtils.isEmpty(queryUsed)) {
+ continue;
+ }
+ // The mapping of a table the query's filtering left out entirely
is empty, so a table it does not
+ // mention at all is a table every query-used partition of which
is uncovered.
+ Map<String, Set<String>> tableMapping =
mtmvRelatedTableIfMapMap.getOrDefault(pctTable, Maps.newHashMap());
+ Set<String> named = Sets.newHashSet();
+ for (Set<String> names : tableMapping.values()) {
+ named.addAll(names);
+ }
+ Set<String> uncovered = Sets.difference(queryUsed, named);
+ if (uncovered.isEmpty()) {
+ continue;
+ }
+ uncoveredByTable.put(pctTable, uncovered);
+ Set<String> recorded = Sets.newHashSet();
+ MTMVRefreshSnapshot refreshSnapshot = mtmv.getRefreshSnapshot();
+ if (refreshSnapshot != null) {
+ for (String mvPartitionName :
rewrittenPlanUsePartitionNameSet) {
+
recorded.addAll(refreshSnapshot.getPctSnapshots(mvPartitionName, new
BaseTableInfo(pctTable)));
+ }
+ }
+ if (!Sets.intersection(uncovered, recorded).isEmpty()) {
+ uncoveredRecorded = true;
+ }
+ }
+ // Narrowing a table's read keeps the MV partitions answering for the
rest, and that only holds while
+ // one input of the query reads the tables it narrows: the pass that
turns the compensation into a plan
+ // puts the filters it is given on one join, so two narrowed inputs
read the pairs both filters reach
+ // and lose the mixed ones -- an a_old x b_kept row, say, when A is
narrowed for a partition the
+ // mapping does not name and B for one the MV no longer holds, or when
one table is narrowed while a
+ // self-join reads it through two inputs, both of which the filter
reaches. Nor can a table be left as
+ // it is while the MV partitions that answered for it are taken out.
So the MV partitions are taken
+ // out, and every table the MV is partitioned on is read for the
query's own partitions, whenever more
+ // than one table is narrowed, a single table is narrowed while the
query reads such a table through
+ // more inputs than that, or a refresh read one of the uncovered
partitions into an MV partition.
+ int narrowedTables = 0;
+ for (MTMVRelatedTableIf pctTable :
mtmv.getMvPartitionInfo().getPctTables()) {
+ BaseColInfo pctInfo = pctInfoMap.get(new BaseTableInfo(pctTable));
+ Set<String> queryUsed =
queryUsedBaseTablePartitionMap.get(pctTable.getFullQualifiers());
+ Set<String> stale = pctInfo == null ? null :
baseTablePartitionNeedUnionNameMap.get(pctInfo);
+ boolean narrowedByStalePass = CollectionUtils.isNotEmpty(stale) &&
!stale.containsAll(queryUsed);
+ if (narrowedByStalePass || uncoveredByTable.containsKey(pctTable))
{
+ narrowedTables++;
+ }
+ }
+ boolean takeMvPartitionsOut = uncoveredRecorded || narrowedTables > 1
+ || (narrowedTables > 0 && pctInputsInQuery > narrowedTables);
+ if (!takeMvPartitionsOut) {
+ // The MV partitions stay, and the uncovered partitions are added
to the base branch beside them.
+ // Such an uncovered partition can answer a group an MV partition
also answers when its keys
+ // meet, in the MV's partition columns, the keys of the base
partitions that MV partition is
+ // recorded with: a merged LIST component {kept (2020,1), bridge
(2038,2)} projected onto d
+ // holds 2020 and 2038, and an uncovered old (2020,2) is read
beside it, so the union would
+ // answer d=2020 twice. A RANGE partition whose range no retained
MV partition covers answers
+ // its own groups and is not recorded here.
+ for (Entry<MTMVRelatedTableIf, Set<String>> entry :
uncoveredByTable.entrySet()) {
+ BaseColInfo pctInfo = pctInfoMap.get(new
BaseTableInfo(entry.getKey()));
+ if (pctInfo == null) {
+ continue;
+ }
+
uncoveredNarrowedBasePartitions.addAll(overlappingUncoveredListPartitions(
+ entry.getKey(), pctInfo, entry.getValue(),
rewrittenPlanUsePartitionNameSet,
Review Comment:
[P2] Exclude removed MV partitions from the retained-key overlap check
This passes every selected MV partition, including those already scheduled
for removal, as the helper's `retainedMvPartitions`. For LIST(d,k), use
`p_stale={(2020-01-01,1),(2037-01-01,1)}`, `p_bridge={(2037-01-01,2)}`,
`p_old={(2020-01-01,2)}`, and `p_fresh={(2038-01-01,1)}` with a 2 YEAR sync
limit. Populate both retained components, refresh `PARTITION BY(d) AS SELECT
d,SUM(v) FROM t GROUP BY d`, then write `p_stale`. Query dates 2020-01-01 and
2038-01-01, grouped by d.
The invalid merged `{2020,2037}` MV partition is removed; only the fresh
2038 partition remains. The base branch reads old plus the stale bucket (the
query filter excludes bridge's 2037 rows), so the valid target is:
```text
UnionAll
Scan MV[fresh 2038 aggregate]
Aggregate(d,sum(v)) -> Filter(d=2020 AND compensation keys) -> Scan t
```
Its groups are disjoint. But the helper sees 2020 in the stale MV partition,
flags `p_old`, and the aggregate guard rejects the rewrite before removal.
Unlike #4359578727's fixed RANGE case, this false overlap comes specifically
from including a removed LIST component. Subtract the MV removal set before
computing retained keys and add this partial-staleness case.
--
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]