Repository navigation
[improvement](mtmv) Read the base partitions an MV partition is recorded with #68648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0dab488
c905ab6
0578b8a
2ec5b28
ddc9f6e
9a0fd79
60ce6cc
141e1e8
bb0a3a1
e87cde5
958c19c
590c8f8
25d37d3
a961cc9
889b957
77ab4b5
08bdc17
7230896
6ad384e
199988c
2779248
a8c0eb8
9a80de8
7a4ad42
e281cff
31cb7f7
8b88015
59a89ef
9925e45
030ff4e
ba6f031
740e22b
92e1da5
6bc3560
e6a298e
ebb3031
142a13c
dbaa4f5
b55558a
8181555
c80a092
2b06171
f80acf9
6c17a81
ef660f7
ac19d76
071fb0d
44c6a7c
2c9c305
6219e6e
c358a6a
f724170
8206571
6b2222a
972f8b8
dc1d04c
1de7a0f
e57d08a
a3cdee3
927f012
2ce15a6
cac7778
8d1ab2b
d880803
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1679,6 +1679,23 @@ public Map<String, Map<MTMVRelatedTableIf, Set<String>>> calculatePartitionMappi | |
| public Map<String, Map<MTMVRelatedTableIf, Set<String>>> calculatePartitionMappings( | ||
| Map<List<String>, Set<String>> queryUsedBaseTablePartitionMap, | ||
| Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots) throws AnalysisException { | ||
| return calculatePartitionMappings(queryUsedBaseTablePartitionMap, pinnedSnapshots, Sets.newHashSet()); | ||
| } | ||
|
|
||
| /** | ||
| * The same, with every MV partition no base partition describes added to {@code undescribedMvPartitions}. | ||
| * | ||
| * <p>Such a partition is one the alignment has not caught up with: a base {@code ADD PARTITION} widens the | ||
| * keys an MV partition covers, so the alignment replaces it with an empty partition of the wider keys, and | ||
| * until that is done its own descriptor is one the base partitions no longer produce. Reading it as no | ||
| * table feeding it would write no rows over the rows it holds, so a refresh has to tell it apart from a | ||
| * partition that really is fed by nothing -- and the default list partition of a table is named in every | ||
| * MV partition's mapping, which is why the mapping being non-empty says nothing about this. | ||
| */ | ||
| public Map<String, Map<MTMVRelatedTableIf, Set<String>>> calculatePartitionMappings( | ||
| Map<List<String>, Set<String>> queryUsedBaseTablePartitionMap, | ||
| Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots, | ||
| Set<String> undescribedMvPartitions) throws AnalysisException { | ||
| if (mvPartitionInfo.getPartitionType() == MTMVPartitionType.SELF_MANAGE) { | ||
| return Maps.newHashMap(); | ||
| } | ||
|
|
@@ -1697,9 +1714,15 @@ public Map<String, Map<MTMVRelatedTableIf, Set<String>>> calculatePartitionMappi | |
| Map<PartitionKeyDesc, Map<MTMVRelatedTableIf, Set<String>>> pctPartitionDescs = MTMVPartitionUtil | ||
| .generateRelatedPartitionDescs(mvPartitionInfo, mvProperties, getPartitionColumns(), | ||
| effectiveFilter, pinnedSnapshots); | ||
| Map<MTMVRelatedTableIf, String> defaultListPartitions = defaultListPartitionsOf(); | ||
| for (Entry<String, PartitionItem> entry : mvPartitionItems.entrySet()) { | ||
| res.put(entry.getKey(), | ||
| pctPartitionDescs.getOrDefault(entry.getValue().toPartitionKeyDesc(), Maps.newHashMap())); | ||
| Map<MTMVRelatedTableIf, Set<String>> matched = | ||
| pctPartitionDescs.get(entry.getValue().toPartitionKeyDesc()); | ||
| if (matched == null) { | ||
| undescribedMvPartitions.add(entry.getKey()); | ||
| } | ||
| res.put(entry.getKey(), withDefaultListPartitions( | ||
| matched == null ? Maps.newHashMap() : matched, defaultListPartitions)); | ||
| } | ||
| if (LOG.isDebugEnabled()) { | ||
| LOG.debug("calculatePartitionMappings use [{}] mills, mvName is [{}]", | ||
|
|
@@ -1708,6 +1731,65 @@ public Map<String, Map<MTMVRelatedTableIf, Set<String>>> calculatePartitionMappi | |
| return res; | ||
| } | ||
|
|
||
| /** | ||
| * The list partition each base table of this MV has that takes the rows no other partition of it claims, | ||
| * by table, or none for a table that has no such partition. | ||
| * | ||
| * <p>Read once per mapping rather than per MV partition: the mapping describes every MV partition and the | ||
| * answer is the table's, not the partition's. The table's partitions are read under its read lock, so | ||
| * that a concurrent ADD or DROP PARTITION cannot be seen half applied -- its name list and the items the | ||
| * walk resolves against it have to come from one state of the table -- and so that this walk is not one | ||
| * more reader of a tree another thread is modifying. | ||
| */ | ||
| private Map<MTMVRelatedTableIf, String> defaultListPartitionsOf() throws AnalysisException { | ||
| Map<MTMVRelatedTableIf, String> res = Maps.newHashMap(); | ||
| for (MTMVRelatedTableIf pctTable : mvPartitionInfo.getPctTables()) { | ||
| if (!(pctTable instanceof OlapTable)) { | ||
| continue; | ||
| } | ||
| OlapTable olapTable = (OlapTable) pctTable; | ||
| if (!(olapTable.getPartitionInfo() instanceof ListPartitionInfo)) { | ||
| continue; | ||
| } | ||
| olapTable.readLock(); | ||
| try { | ||
| for (String partitionName : olapTable.getPartitionNames()) { | ||
| if (olapTable.getPartitionItemOrAnalysisException(partitionName).isDefaultPartition()) { | ||
| res.put(pctTable, partitionName); | ||
| break; | ||
| } | ||
| } | ||
| } finally { | ||
| olapTable.readUnlock(); | ||
| } | ||
| } | ||
| return res; | ||
| } | ||
|
|
||
| /** | ||
| * One MV partition's mapping, with every base table's default list partition named in it. | ||
| * | ||
| * <p>Such a partition holds rows for every key its table can be read by, so it belongs to every MV | ||
| * partition that reads the table -- not only to the one its own key, the sentinel those rows were placed | ||
| * by, maps to. Naming it everywhere is what the read and the record have to agree on: the refresh reads | ||
| * the rows of it that belong to the MV partition being refreshed, and the partition is recorded among the | ||
| * ones that partition is read through, so an insert into it leaves that MV partition out of sync instead | ||
| * of changing nothing the MV compares. | ||
| */ | ||
| private Map<MTMVRelatedTableIf, Set<String>> withDefaultListPartitions( | ||
| Map<MTMVRelatedTableIf, Set<String>> mapping, Map<MTMVRelatedTableIf, String> defaultListPartitions) { | ||
| if (defaultListPartitions.isEmpty()) { | ||
| return mapping; | ||
| } | ||
| Map<MTMVRelatedTableIf, Set<String>> res = Maps.newHashMap(mapping); | ||
| for (Entry<MTMVRelatedTableIf, String> entry : defaultListPartitions.entrySet()) { | ||
| Set<String> partitions = Sets.newHashSet(res.getOrDefault(entry.getKey(), Sets.newHashSet())); | ||
| partitions.add(entry.getValue()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Cover default LIST rows during base-side union compensation. In a two-table join MV partitioned by left keys 1 and 3, let the right table's default partition hold rows for both keys. After a key-3 insert, refresh only MV key1, then query both keys with union rewrite enabled. This fan-out makes key1 valid and key3 stale, so compensation removes MV key3 and unions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7ef1678, in the direction you point at and one scope short of it. The compensation now carries the MV partitions it removes into the predicate context, and a default partition is read through the ranges of those MV partitions rather than through its own key, so the join branch for a key that lives only in the default partition is no longer empty. The shortfall to be straight about: the ranges are written on one base column, so this covers an MV whose partition column is that column -- Verification, as with the last two threads: the rewrite suites and the unit tests are green (they say the covered case still rewrites), but I could not build a local case that reaches this branch, since a partly usable MV is refused by
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partly fixed in 0dbc408, and one part still deliberately left out. What is fixed is the case where the base key is not the MV's own column name: What is still out is the multi-column MV (
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An update on the multi-column half I called the next step: it turns out there is nothing to make position-aware, because that shape cannot be created.
So the aliased-column case is what this thread had left, and it is fixed in 0dbc408. |
||
| res.put(entry.getKey(), partitions); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Give default-only keys an MV destination before treating this fanout as complete. For a LIST(k) base with p1=(1) and p_default, a committed k=2 row lands in p_default. Descriptor generation gives the MV only key-1 and synthetic-minimum partitions; adding p_default to both mappings records a dependency, but COMPLETE refresh filters each scan by its MV key, so neither reads k=2. The partitions can still appear synchronized; an unrestricted query using both base partitions can rewrite to this MV and omit the committed k=2 group. The added regression covers a default row sharing an explicit MV key; cover this distinct key and either represent it in the MV or refuse the rewrite. This representation gap predates the fanout change but remains in this path.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, and I agree it is a representation gap rather than anything this PR introduced -- but I do not think it is reachable by the direction this PR takes, so I have left it alone this round rather than guessed. Why the merge and the read-scope work do not reach it: the MV's own partition set comes from the base partitions' keys projected onto the MV's partition column. A key that only ever lived in the default partition has no base partition describing it, so it has no MV partition -- and the refresh reads each MV partition through its own key range, so a key with no range is read by nobody. That is your k=2: So it is one of two deliberate choices, and both are user-visible enough that I would rather ask than take one:
I did not touch either this round. If you want one in this PR, 1 is the smaller change and the one I would take; tell me and I will do it in the next round.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took your first direction in 67048fe: the rewrite is refused, the shape is not. Confirmed first, and it is worse than a narrow corner -- measured on
Why the MV rather than the partitions that read that table: which keys the default partition holds is not in the metadata, so "a query whose keys are all covered by the MV's partitions" cannot be decided. Scoping it to the partitions that read that table would not help either -- the k=2 row is read out of The regression case in |
||
| } | ||
| return res; | ||
| } | ||
|
|
||
| public ConcurrentLinkedQueue<MTMVTask> getHistoryTasks() { | ||
| return jobInfo.getHistoryTasks(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1293,6 +1293,13 @@ private void executePartitionBasedRefresh(MTMVRefreshContext context, RefreshMod | |
| int end = start + refreshPartitionNum; | ||
| Set<String> execPartitionNames = Sets.newHashSet(partitions | ||
| .subList(start, Math.min(end, partitions.size()))); | ||
| // What this batch reads and what it records are the same set, both decided here from the one | ||
| // mapping the snapshots below are generated from; see MTMVPartitionUtil#mappedBasePartitions. | ||
| Map<BaseTableInfo, Set<String>> readableBasePartitions = mtmv.isIvm() | ||
| ? null : MTMVPartitionUtil.mappedBasePartitions(tableWithPartKey, context, execPartitionNames); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Keep absent IVM PCT inputs inside the recorded batch scope The IVM exemption is unsafe for LIST components newly admitted by this PR. Use binlog-enabled OLAP UNIQUE(d,k) MOW tables partitioned by LIST(d): A.kept={2020,2038}, C.kept={2020,2038,2039}, B.old={2020}, B.future={2040} (dates are January 1). Populate one 2020 row in each table and create an IVM MV partitioned by d over For COMPLETE on M, the mapping records B={} and M therefore stores the 2020 join row without recording B.old. After
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed by reading the three links, and it goes on the follow-up list with the external-PCT thread above rather than into this change, because closing it is a decision about the mapping that the two share:
Both ways to close it change what the mapping means, which is why I would rather settle it with a test than here: scoping that read to the batch's mapping makes the MV partition lose rows the join does produce for the keys it does cover, while recording what the read takes makes the mapping -- and the incremental delta that reads it -- admit a partition the window dropped. That is the same fork as the external-PCT finding above, whose read is unscoped for the same reason (
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Closed here rather than in a follow-up: 8d1ab2b preserves an explicit empty reset set for every absent PCT input.
The regression is So the fork I described earlier is settled on the read side: the batch's scope is the mapping, and a table the mapping does not name is read as nothing -- the MV loses no row the batch's keys produce, because the regression's other component is still built with all three inputs read. |
||
| if (!mtmv.isIvm() && hasOlapPctTable()) { | ||
| requireDescribedPartitions(context, execPartitionNames); | ||
| } | ||
| Map<BaseTableInfo, Set<Long>> batchResetPartitionIds = useIvmFallbackStreams | ||
| ? collectPctResetPartitionIds(context, execPartitionNames) : Maps.newHashMap(); | ||
| Optional<IvmRewriteContext> rewriteContext = Optional.empty(); | ||
|
|
@@ -1313,7 +1320,8 @@ private void executePartitionBasedRefresh(MTMVRefreshContext context, RefreshMod | |
| execPartitionNames); | ||
| try { | ||
| IvmPlanSignature batchPlanSignature = refreshPartitionsWithRetry( | ||
| execPartitionNames, tableWithPartKey, rewriteContext, refreshMode); | ||
| execPartitionNames, tableWithPartKey, readableBasePartitions, rewriteContext, | ||
| refreshMode); | ||
| if (capturePlanSignature) { | ||
| batchPlanSignature = Objects.requireNonNull(batchPlanSignature, | ||
| "IVM COMPLETE refresh did not produce a plan signature"); | ||
|
|
@@ -1400,10 +1408,11 @@ private boolean hasUnusableIvmStream() { | |
| * Whether a stream this partition refresh will read is missing or unusable. | ||
| * | ||
| * <p>The set is narrower than the plan, and which tables are in it depends on the partitions being | ||
| * refreshed: a PCT table that no refreshed partition's mapping names keeps its place in the plan as | ||
| * an ordinary scan, so its stream is never read, and a table outside the plan's own tables is not | ||
| * scanned at all. Judging the whole plan instead sends a partition refresh that would have worked to | ||
| * a full rebuild whenever such an unread stream is missing. | ||
| * refreshed: a PCT table that no refreshed partition's mapping names is read as no rows at all -- its | ||
| * reset set is empty, and the rewriter answers it with an empty relation rather than a stream scan -- | ||
| * so its stream is never read, and a table outside the plan's own tables is not scanned at all. | ||
| * Judging the whole plan instead sends a partition refresh that would have worked to a full rebuild | ||
| * whenever such an unread stream is missing. | ||
| */ | ||
| private boolean hasUnusableIvmStreamForPartitions(MTMVRefreshContext context, | ||
| List<String> mvPartitionNames) { | ||
|
|
@@ -1496,9 +1505,67 @@ private void reconcileIvmStreams(ConnectContext ctx) throws JobException { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether any of the tables the MV is partitioned on is an olap one, which is where the check below can | ||
| * read anything. It reads a partition no base partition describes as the alignment having fallen behind an | ||
| * olap table's change, and that is a state only an olap table has: an external table describes its | ||
| * partitions from the snapshot the read is taken at, and one it cannot describe -- a row whose partition | ||
| * key is null, say -- is not a state the alignment can catch up with at all. So the check is taken where | ||
| * its reading can be true, and an MV partitioned on external tables alone is left to the read's own scope. | ||
| */ | ||
| private boolean hasOlapPctTable() throws AnalysisException { | ||
| for (MTMVRelatedTableIf pctTable : mtmv.getMvPartitionInfo().getPctTables()) { | ||
| if (pctTable instanceof OlapTable) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Whether each MV partition of this batch is one the mapping describes, i.e. one that some base partition's | ||
| * keys name. A partition nothing describes is one the alignment has not caught up with: a base `ADD | ||
| * PARTITION` widens the keys an MV partition covers, which replaces it with an empty partition of the new | ||
| * keys, and if that lands between the alignment and this mapping the partition is read as no table feeding | ||
| * it and the batch writes no rows over the rows it holds. The batch is not read at all then: the task tries | ||
| * again, and trying again realigns. | ||
| */ | ||
| private void requireDescribedPartitions(MTMVRefreshContext context, Set<String> execPartitionNames) | ||
| throws AnalysisException { | ||
| for (String mvPartitionName : execPartitionNames) { | ||
| if (context.getUndescribedMvPartitions().contains(mvPartitionName)) { | ||
| throw new AnalysisException("MV partition " + mvPartitionName + " of " + mtmv.getName() | ||
| + " is described by no base partition: the alignment has not caught up with a base " | ||
| + "table change. Refreshing it would write no rows over the rows it holds."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The partitions each PCT input of this batch is read from, by table, with an explicit empty set for a | ||
| * table the batch's mapping does not name. | ||
| * | ||
| * <p>An absent table is one whose retained base partitions share no key with the MV partitions this | ||
| * batch refreshes -- after a sync window dropped them, say -- so the batch must read no rows of it. | ||
| * Leaving the entry out instead is what makes the read differ from the record: the rewriter has no | ||
| * reset for the table and falls back to its ordinary scan, which reads the partitions the window | ||
| * dropped, while the mapping records none of them. The rows land in the MV partition and the snapshot | ||
| * says the table was not read, so a later change to them is never compared and a later delta never | ||
| * looks at them. | ||
| * | ||
| * <p>Seeded the same way {@link MTMVPartitionUtil#generateRelatedBasePartitionIds} seeds the delta's | ||
| * scope, and for the same reason: the empty set says "this table may not be read at all", which is not | ||
| * the same statement as the table's absence. The pre-seeded set is not looked up for a table the MV | ||
| * excludes from trigger tracking -- the rewriter returns its scan before consulting the reset. | ||
| */ | ||
| private Map<BaseTableInfo, Set<Long>> collectPctResetPartitionIds(MTMVRefreshContext context, | ||
| Set<String> execPartitionNames) throws AnalysisException { | ||
| Map<BaseTableInfo, Set<Long>> resetPartitionIds = Maps.newHashMap(); | ||
| for (MTMVRelatedTableIf pctTable : mtmv.getMvPartitionInfo().getPctTables()) { | ||
| if (pctTable instanceof OlapTable) { | ||
| resetPartitionIds.put(new BaseTableInfo(pctTable), Sets.newHashSet()); | ||
| } | ||
| } | ||
| for (String mvPartitionName : execPartitionNames) { | ||
| for (Entry<MTMVRelatedTableIf, Set<String>> entry | ||
| : context.getByPartitionName(mvPartitionName).entrySet()) { | ||
|
|
@@ -1517,11 +1584,11 @@ private Map<BaseTableInfo, Set<Long>> collectPctResetPartitionIds(MTMVRefreshCon | |
| } | ||
|
|
||
| private IvmPlanSignature refreshPartitionsWithRetry(Set<String> execPartitionNames, | ||
| Map<TableIf, String> tableWithPartKey, | ||
| Map<TableIf, String> tableWithPartKey, Map<BaseTableInfo, Set<String>> readableBasePartitions, | ||
| Optional<IvmRewriteContext> rewriteContext, RefreshMode refreshMode) | ||
| throws Exception { | ||
| return executeWithRetry(() -> refreshPartitions(execPartitionNames, tableWithPartKey, | ||
| rewriteContext, refreshMode), | ||
| readableBasePartitions, rewriteContext, refreshMode), | ||
| "partition refresh, execPartitionNames=" + execPartitionNames); | ||
| } | ||
|
|
||
|
|
@@ -1577,7 +1644,7 @@ private boolean needRetry(Exception exception) { | |
| } | ||
|
|
||
| private IvmPlanSignature refreshPartitions(Set<String> refreshPartitionNames, | ||
| Map<TableIf, String> tableWithPartKey, | ||
| Map<TableIf, String> tableWithPartKey, Map<BaseTableInfo, Set<String>> readableBasePartitions, | ||
| Optional<IvmRewriteContext> rewriteContext, RefreshMode refreshMode) | ||
| throws Exception { | ||
| // Create the MTMV context before parsing the MV definition SQL so SET_VAR hints | ||
|
|
@@ -1601,7 +1668,8 @@ private IvmPlanSignature refreshPartitions(Set<String> refreshPartitionNames, | |
| // if SELF_MANAGE mv, only have default partition, will not have partitionItem, so we give empty set | ||
| UpdateMvByPartitionCommand command = UpdateMvByPartitionCommand | ||
| .from(mtmv, mtmv.getMvPartitionInfo().getPartitionType() != MTMVPartitionType.SELF_MANAGE | ||
| ? refreshPartitionNames : Sets.newHashSet(), tableWithPartKey, statementContext); | ||
| ? refreshPartitionNames : Sets.newHashSet(), tableWithPartKey, statementContext, | ||
| readableBasePartitions); | ||
| setupComputeGroup(mtmvCtx); | ||
| AtomicReference<IvmPlanSignature> signatureRef = new AtomicReference<>(); | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Check every MV partition mapped from a default LIST base partition before allowing rewrite. This fan-out puts p_default in multiple MV mappings, but MTMVRewriteUtil.getPctToMv inverts them into one String and keeps only an arbitrary destination. With explicit LIST tuples (2020,2) and (2021,2) plus default rows for (2020,3) and (2021,3), an insert into p_default dirties both MV partitions. Refresh only the inverse-selected partition, then query the other default-only tuple: Filter(d=other AND k=3) -> Scan(base[p_default]) can rewrite to Filter(d=other AND k=3) -> Scan(MV[other stale partition]) because eligibility checks only the refreshed destination. Setting enable_materialized_view_union_rewrite=false skips the later invalid-partition removal, so this can return stale data. Preserve the full inverse set or reject ambiguous default mappings for rewrite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5ba21b7, by preserving the whole inverse set.
MTMVRewriteUtil.getPctToMvanswers each base partition with the set of MV partitions that read it now, and the rewrite's partition selection adds all of them, so a query that reads a default partition is held against every MV partition its rows can be in rather than against whichever one the map happened to keep. The UT covers the fan-out (t1_defaultread bymv_aandmv_b, answered with both).That is the other half of the fan-out the previous commit introduced, and it is what the mapping's own contract says: the mapping is what the read and the record agree on, so anything inverting it has to keep all of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 0dbc408, on the side that cannot drop rows. Preserving the whole inverse set made every MV partition that reads a base partition a candidate, but the eligibility still answered a query when only some of those partitions were valid. With
enable_materialized_view_union_rewriteoff there is nothing to compensate the others: the MV alone answers the query and reads the partitions that are not valid as they are.getMTMVCanRewritePartitionsnow returns no partition for a query unless every MV partition its base partitions are mapped from is valid, when the union rewrite is off. Read as a subset rather than as a count, since a partition inside its grace period is answered usable without having been compared and is not necessarily one the query reads. With the union rewrite on (the default) a partially valid answer is still a complete one, and the valid partitions still answer.