This is an automated email from the ASF dual-hosted git repository.
CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 7866f5a36c0 [fix](mtmv) Batch non-empty partition checks during MV
compensation (#69966)
7866f5a36c0 is described below
commit 7866f5a36c02e15dc014aaef30e537a395f4059d
Author: Calvin Kirs <[email protected]>
AuthorDate: Tue Nov 3 22:29:08 2026 +0800
[fix](mtmv) Batch non-empty partition checks during MV compensation (#69966)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
Cloud MV partition compensation checks each valid mapped MV partition
through a singleton non-empty request. Cached-empty or unknown
partitions can therefore cause many serial MS version requests, repeated
across related partition tables, even when the MV is not ultimately
selected.
Collect unique mapped valid MV partition IDs and call the existing bulk
interface in batches of at most 5,000. Reuse the returned ID set across
related tables, preserving partition removal, base-table compensation,
cache handling and exception propagation.
The 5,000-partition cap is a fixed code constant, not a runtime
configuration. It bounds the three request ID lists to at most 165,000
bytes at the worst protobuf integer width, plus the remaining request
fields. This is a request-size guard, not a claimed performance-optimal
batch size. Tests verify that 1,020 candidates use one bulk call and
10,001 use three; these are mocked interface call counts, not a live
Cloud benchmark.
---
.../rules/exploration/mv/PartitionCompensator.java | 27 ++-
.../mv/PartitionCompensatorBatchTest.java | 252 +++++++++++++++++++++
.../exploration/mv/PartitionCompensatorTest.java | 6 +-
3 files changed, 277 insertions(+), 8 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
index 33636472c76..97b05fc3d4f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
@@ -38,6 +38,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import org.apache.logging.log4j.LogManager;
@@ -52,6 +53,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Handle materialized view partition union compensate handler
@@ -65,6 +67,8 @@ public class PartitionCompensator {
// It means all partitions are used when query
public static final Collection<Pair<RelationId, Set<String>>>
ALL_PARTITIONS_LIST =
ImmutableList.of(ALL_PARTITIONS);
+ // Bound the number of partition versions requested by each non-empty
check.
+ private static final int NON_EMPTY_PARTITION_BATCH_SIZE = 5000;
/** Whether an async MV references a query relation with unavailable
partition coverage. */
public static boolean hasUnknownPartitionCoverage(MaterializationContext
materializationContext,
@@ -122,6 +126,18 @@ public class PartitionCompensator {
}
Map<MTMVRelatedTableIf, Map<String, Set<String>>>
mtmvRelatedTableIfMapMap
=
materializationContext.calculatePartitionMappings(queryUsedBaseTablePartitionMap);
+ Set<String> mappedMvPartitionNames = new HashSet<>();
+ mtmvRelatedTableIfMapMap.values().forEach(mapping ->
mappedMvPartitionNames.addAll(mapping.keySet()));
+ List<Long> mvPartitionIdsToCheck = mvValidPartitions.stream()
+ .filter(partition ->
mappedMvPartitionNames.contains(partition.getName()))
+ .map(Partition::getId)
+ .distinct()
+ .collect(Collectors.toList());
+ // An MV partition can be mapped to several PCT tables. Check it once
and reuse the result.
+ Set<Long> nonEmptyMvPartitionIds = new HashSet<>();
+ for (List<Long> batch : Lists.partition(mvPartitionIdsToCheck,
NON_EMPTY_PARTITION_BATCH_SIZE)) {
+
nonEmptyMvPartitionIds.addAll(mtmv.selectNonEmptyPartitionIds(batch,
Optional.empty()));
+ }
boolean allCompensateIsNull = true;
Map<BaseTableInfo, Set<String>> mvPartitionNeedRemoveNameMap = new
HashMap<>();
@@ -137,9 +153,10 @@ public class PartitionCompensator {
Set<String> relatedTableUsedPartitionSet
=
queryUsedBaseTablePartitionMap.get(relatedTable.getFullQualifiers());
Pair<Pair<BaseTableInfo, Set<String>>, Pair<BaseColInfo,
Set<String>>> needCompensatePartitions
- = getNeedCompensatePartitions(mvValidPartitions,
relatedTableUsedPartitionSet,
- rewrittenPlanUsePartitionNameSet, pctInfoMap.get(new
BaseTableInfo(relatedTable)),
- partitionMapping.getValue(), materializationContext);
+ = getNeedCompensatePartitions(mvValidPartitions,
nonEmptyMvPartitionIds,
+ relatedTableUsedPartitionSet,
rewrittenPlanUsePartitionNameSet,
+ pctInfoMap.get(new BaseTableInfo(relatedTable)),
partitionMapping.getValue(),
+ materializationContext);
allCompensateIsNull &= needCompensatePartitions == null;
if (needCompensatePartitions == null) {
continue;
@@ -178,6 +195,7 @@ public class PartitionCompensator {
private static Pair<Pair<BaseTableInfo, Set<String>>, Pair<BaseColInfo,
Set<String>>> getNeedCompensatePartitions(
Collection<Partition> mvValidPartitions,
+ Set<Long> nonEmptyMvPartitionIds,
Set<String> queryUsedBaseTablePartitionNameSet,
Set<String> rewrittenPlanUsePartitionNameSet,
BaseColInfo relatedPartitionTable,
@@ -196,8 +214,7 @@ public class PartitionCompensator {
Set<String> relatedBaseTablePartitions =
partitionMapping.get(mvValidPartition.getName());
if (relatedBaseTablePartitions != null) {
mvValidBaseTablePartitionNameSet.addAll(relatedBaseTablePartitions);
- if
(!mtmv.selectNonEmptyPartitionIds(ImmutableList.of(mvValidPartition.getId()),
- Optional.empty()).isEmpty()) {
+ if (nonEmptyMvPartitionIds.contains(mvValidPartition.getId()))
{
mvValidHasDataRelatedBaseTableNameSet.addAll(relatedBaseTablePartitions);
}
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorBatchTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorBatchTest.java
new file mode 100644
index 00000000000..ea839496586
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorBatchTest.java
@@ -0,0 +1,252 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.exploration.mv;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.MTMV;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.PartitionInfo;
+import org.apache.doris.catalog.PartitionType;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Pair;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.mtmv.BaseColInfo;
+import org.apache.doris.mtmv.BaseTableInfo;
+import org.apache.doris.mtmv.MTMVPartitionInfo;
+import org.apache.doris.mtmv.MTMVRelatedTableIf;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.plans.Plan;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class PartitionCompensatorBatchTest {
+ @ParameterizedTest
+ @ValueSource(ints = {1, 1020, 4999, 5000, 5001, 10001})
+ public void testBoundedBatchesAreSharedAcrossRelatedTables(int
partitionCount) throws Exception {
+ Fixture fixture = new Fixture();
+ for (long id = 1; id <= partitionCount; id++) {
+ fixture.addPartition(id);
+ }
+ // Duplicate candidates must not cause repeated version requests.
+ fixture.validPartitions.add(fixture.validPartitions.get(0));
+ fixture.queryUsedPartitions.replaceAll((table, names) ->
ImmutableSet.of("p1"));
+
+ List<List<Long>> requests = new ArrayList<>();
+
Mockito.when(fixture.mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> {
+ List<Long> ids =
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0));
+ Assertions.assertEquals(Optional.empty(),
invocation.getArgument(1));
+ Assertions.assertTrue(ids.size() <= 5000);
+ requests.add(ids);
+ return ids;
+ });
+
+ Pair<Map<BaseTableInfo, Set<String>>, Map<BaseColInfo, Set<String>>>
result = fixture.calculate();
+ Assertions.assertNotNull(result);
+ Assertions.assertTrue(result.key().isEmpty());
+ Assertions.assertTrue(result.value().isEmpty());
+ Assertions.assertEquals((partitionCount + 4999) / 5000,
requests.size());
+ List<Long> requestedIds = new ArrayList<>();
+ requests.forEach(requestedIds::addAll);
+ Assertions.assertEquals(partitionCount, requestedIds.size());
+ Set<Long> uniqueRequestedIds = new HashSet<>(requestedIds);
+ Assertions.assertEquals(partitionCount, uniqueRequestedIds.size());
+ for (long id = 1; id <= partitionCount; id++) {
+ Assertions.assertTrue(uniqueRequestedIds.contains(id));
+ }
+ }
+
+ @Test
+ public void testMixedEmptyPartitionsPreserveCompensation() throws
Exception {
+ Fixture fixture = new Fixture();
+ fixture.addPartition(1L);
+ fixture.addPartition(2L);
+
fixture.queryUsedPartitions.put(fixture.firstTable.getFullQualifiers(),
ImmutableSet.of("p2", "t1_new"));
+
fixture.queryUsedPartitions.put(fixture.secondTable.getFullQualifiers(),
ImmutableSet.of("p2", "t2_new"));
+
Mockito.when(fixture.mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenReturn(ImmutableList.of(2L));
+
+ Pair<Map<BaseTableInfo, Set<String>>, Map<BaseColInfo, Set<String>>>
result = fixture.calculate();
+ Assertions.assertNotNull(result);
+ Assertions.assertTrue(result.key().isEmpty());
+ Assertions.assertEquals(2, result.value().size());
+ result.value().values().forEach(names ->
+ Assertions.assertEquals(ImmutableSet.of("t1_new", "t2_new"),
names));
+
Mockito.verify(fixture.mtmv).selectNonEmptyPartitionIds(ImmutableList.of(1L,
2L), Optional.empty());
+
+ // The only non-empty MV partition does not cover p1, so it cannot
answer this query.
+ fixture.queryUsedPartitions.replaceAll((table, names) ->
ImmutableSet.of("p1"));
+ Assertions.assertNull(fixture.calculate());
+ }
+
+ @Test
+ public void testAllEmptyPartitionsRejectMv() throws Exception {
+ Fixture fixture = new Fixture();
+ fixture.addPartition(1L);
+ fixture.addPartition(2L);
+
Mockito.when(fixture.mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenReturn(ImmutableList.of());
+
+ Assertions.assertNull(fixture.calculate());
+
Mockito.verify(fixture.mtmv).selectNonEmptyPartitionIds(ImmutableList.of(1L,
2L), Optional.empty());
+ }
+
+ @Test
+ public void testOnlyMappedValidPartitionsAreChecked() throws Exception {
+ Fixture fixture = new Fixture();
+ fixture.addPartition(1L);
+ fixture.addPartition(2L);
+ fixture.addPartition(3L);
+ fixture.validPartitions.remove(2);
+ fixture.partitionMappings.values().forEach(mapping ->
mapping.remove("mv_p2"));
+
Mockito.when(fixture.mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation ->
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0)));
+
+ Assertions.assertNotNull(fixture.calculate());
+
Mockito.verify(fixture.mtmv).selectNonEmptyPartitionIds(ImmutableList.of(1L),
Optional.empty());
+ }
+
+ @Test
+ public void testNoMappedValidPartitionsSkipVersionRequests() throws
Exception {
+ Fixture fixture = new Fixture();
+ fixture.addPartition(1L);
+ fixture.partitionMappings.values().forEach(Map::clear);
+
+ Assertions.assertNull(fixture.calculate());
+ Mockito.verify(fixture.mtmv, Mockito.never())
+ .selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any());
+ }
+
+ @Test
+ public void testNoValidPartitionsSkipVersionRequests() throws Exception {
+ Fixture fixture = new Fixture();
+
+ Assertions.assertNull(fixture.calculate());
+ Mockito.verify(fixture.mtmv, Mockito.never())
+ .selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any());
+ }
+
+ @Test
+ public void testFailedBatchDoesNotReturnPartialCompensation() throws
Exception {
+ Fixture fixture = new Fixture();
+ for (long id = 1; id <= 5001; id++) {
+ fixture.addPartition(id);
+ }
+ RuntimeException failure = new RuntimeException("get version from meta
service failed");
+
Mockito.when(fixture.mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation ->
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0)))
+ .thenThrow(failure);
+
+ Assertions.assertSame(failure,
Assertions.assertThrows(RuntimeException.class, fixture::calculate));
+ Mockito.verify(fixture.mtmv, Mockito.times(2))
+ .selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any());
+ }
+
+ private static class Fixture {
+ private final MTMV mtmv = Mockito.mock(MTMV.class);
+ private final List<Partition> validPartitions = new ArrayList<>();
+ private final Map<MTMVRelatedTableIf, Map<String, Set<String>>>
partitionMappings = new HashMap<>();
+ private final Map<List<String>, Set<String>> queryUsedPartitions = new
HashMap<>();
+ private final AsyncMaterializationContext materializationContext
+ = Mockito.mock(AsyncMaterializationContext.class);
+ private final CascadesContext cascadesContext =
Mockito.mock(CascadesContext.class);
+ private final Plan rewrittenPlan = Mockito.mock(Plan.class);
+ private final MTMVRelatedTableIf firstTable;
+ private final MTMVRelatedTableIf secondTable;
+
+ Fixture() throws AnalysisException {
+ CatalogIf<?> catalog = Mockito.mock(CatalogIf.class);
+ Mockito.when(catalog.getId()).thenReturn(1L);
+ Mockito.when(catalog.getName()).thenReturn("internal");
+ DatabaseIf<?> database = Mockito.mock(DatabaseIf.class);
+ Mockito.when(database.getId()).thenReturn(2L);
+ Mockito.when(database.getFullName()).thenReturn("db");
+ Mockito.when(database.getCatalog()).thenReturn(catalog);
+ firstTable = mockTable("t1", 10L, database);
+ secondTable = mockTable("t2", 20L, database);
+ partitionMappings.put(firstTable, new HashMap<>());
+ partitionMappings.put(secondTable, new HashMap<>());
+ queryUsedPartitions.put(firstTable.getFullQualifiers(),
ImmutableSet.of("p1"));
+ queryUsedPartitions.put(secondTable.getFullQualifiers(),
ImmutableSet.of("p1"));
+
+ Mockito.when(mtmv.getId()).thenReturn(100L);
+ Mockito.when(mtmv.getName()).thenReturn("mv");
+ Mockito.when(mtmv.getDatabase()).thenReturn(database);
+ PartitionInfo partitionInfo = Mockito.mock(PartitionInfo.class);
+
Mockito.when(partitionInfo.getType()).thenReturn(PartitionType.RANGE);
+ Mockito.when(mtmv.getPartitionInfo()).thenReturn(partitionInfo);
+ MTMVPartitionInfo mvPartitionInfo =
Mockito.mock(MTMVPartitionInfo.class);
+
Mockito.when(mvPartitionInfo.getPctTables()).thenReturn(ImmutableSet.of(firstTable,
secondTable));
+ List<BaseColInfo> pctInfos = ImmutableList.of(
+ new BaseColInfo("dt", new BaseTableInfo(firstTable)),
+ new BaseColInfo("dt", new BaseTableInfo(secondTable)));
+ Mockito.when(mvPartitionInfo.getPctInfos()).thenReturn(pctInfos);
+
Mockito.when(mtmv.getMvPartitionInfo()).thenReturn(mvPartitionInfo);
+
+ Mockito.when(materializationContext.getMtmv()).thenReturn(mtmv);
+
Mockito.when(materializationContext.calculatePartitionMappings(ArgumentMatchers.any()))
+ .thenReturn(partitionMappings);
+ StatementContext statementContext =
Mockito.mock(StatementContext.class);
+ Map<BaseTableInfo, Collection<Partition>> canRewritePartitions
+ = ImmutableMap.of(new BaseTableInfo(mtmv),
validPartitions);
+ Mockito.when(statementContext.getMvCanRewritePartitionsMap())
+ .thenReturn(canRewritePartitions);
+
Mockito.when(cascadesContext.getStatementContext()).thenReturn(statementContext);
+
Mockito.when(rewrittenPlan.collectToList(ArgumentMatchers.any())).thenReturn(ImmutableList.of());
+ }
+
+ private void addPartition(long id) {
+ String name = "mv_p" + id;
+ validPartitions.add(new Partition(id, name, null, null));
+ partitionMappings.values().forEach(mapping -> mapping.put(name,
ImmutableSet.of("p" + id)));
+ }
+
+ private Pair<Map<BaseTableInfo, Set<String>>, Map<BaseColInfo,
Set<String>>> calculate()
+ throws AnalysisException {
+ return PartitionCompensator.calcInvalidPartitions(
+ queryUsedPartitions, rewrittenPlan,
materializationContext, cascadesContext);
+ }
+
+ private static MTMVRelatedTableIf mockTable(String name, long id,
DatabaseIf<?> database) {
+ MTMVRelatedTableIf table = Mockito.mock(MTMVRelatedTableIf.class);
+ Mockito.when(table.getId()).thenReturn(id);
+ Mockito.when(table.getName()).thenReturn(name);
+ Mockito.when(table.getDatabase()).thenReturn(database);
+
Mockito.when(table.getFullQualifiers()).thenReturn(ImmutableList.of("internal",
"db", name));
+ return table;
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
index b75a752bc8c..e6581055112 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
@@ -455,7 +455,7 @@ public class PartitionCompensatorTest extends
TestWithFeService {
Mockito.when(mvPctInfo.getPctInfos()).thenReturn(ImmutableList.of(colInfo1,
colInfo2));
// All MV partitions contain data
Mockito.when(mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
- .thenReturn(ImmutableList.of(1L));
+ .thenAnswer(invocation ->
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0)));
AsyncMaterializationContext matCtx =
Mockito.mock(AsyncMaterializationContext.class);
Mockito.when(matCtx.getMtmv()).thenReturn(mtmv);
@@ -512,7 +512,7 @@ public class PartitionCompensatorTest extends
TestWithFeService {
Mockito.when(mtmv.getId()).thenReturn(100L);
Mockito.when(mtmv.getDatabase()).thenReturn(mvDb);
Mockito.when(mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
- .thenReturn(ImmutableList.of(1L));
+ .thenAnswer(invocation ->
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0)));
long validMvPartitionId = 101L;
long partiallyStaleMvPartitionId = 102L;
@@ -590,7 +590,7 @@ public class PartitionCompensatorTest extends
TestWithFeService {
Mockito.when(mtmv.getId()).thenReturn(100L);
Mockito.when(mtmv.getDatabase()).thenReturn(mvDb);
Mockito.when(mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(),
ArgumentMatchers.any()))
- .thenReturn(ImmutableList.of(1L));
+ .thenAnswer(invocation ->
ImmutableList.copyOf(invocation.<Collection<Long>>getArgument(0)));
long mvP20260301Id = 101L;
long mvP20260401Id = 102L;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]