This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch branch-4.1.4
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1.4 by this push:
     new 3661553280e branch-4.1.4: [fix](mtmv) Do not rebuild the whole MV when 
a partition has no snapshot (#68237)
3661553280e is described below

commit 3661553280e77e21e95b5fd4375c3aa2c0de97f9
Author: yujun <[email protected]>
AuthorDate: Sun Sep 20 12:09:16 2026 +0800

    branch-4.1.4: [fix](mtmv) Do not rebuild the whole MV when a partition has 
no snapshot (#68237)
    
    ### What problem does this PR solve?
    
    Since the refresh baseline check was added (#64041),
    `MTMVTask#calculateNeedRefreshPartitions()` returns **every** MV
    partition as soon as the refresh snapshot does not cover all current MV
    partitions.
    
    Partition sync runs before that check and adds an MV partition for a
    base partition that just appeared. The new partition legitimately has no
    snapshot yet, so the check fails on it, and adding a single base-table
    partition turns the refresh into a full rebuild of the MV. The
    partitions that were already in sync are not skipped at all: the
    per-partition comparison sits below the early return and never runs.
    
    Observed on a pct materialized view: the base table gains one partition,
    and `NeedRefreshPartitions` then covers every partition with
    `RefreshMode=COMPLETE`, instead of only the new partition with
    `PARTIAL`. Every MV whose base table gains partitions routinely is hit
    on each such refresh (daily partitions, external catalog tables).
    
    ### What changed
    
    - `MTMV#hasRefreshSnapshot()` (renamed from
    `hasCompleteRefreshSnapshot()`, a name that no longer described it)
    reports whether a refresh baseline exists at all - the snapshot map is
    not empty - and tolerates a null snapshot.
    - `MTMVTask#calculateNeedRefreshPartitions()` keeps the early return
    only for a baseline invalidated as a whole, which is what `ALTER ...
    excluded_trigger_tables` leaves behind (it empties the snapshot map), so
    that case still rebuilds. A partition that partition sync has just added
    is decided by the per-partition comparison instead.
    
    ### Test
    
    Unit tests:
    - `MTMVTest.testHasRefreshSnapshotIgnoresPartitionsWithoutSnapshot`: a
    snapshot for `p1` with partitions `{p1, p2}` counts as a baseline; an
    emptied map and a null snapshot do not.
    -
    
`MTMVTaskTest.testCalculateNeedRefreshPartitionsKeepsSyncedPartitionsWhenOneHasNoSnapshot`:
    `REFRESH MATERIALIZED VIEW ... AUTO` with an existing baseline keeps the
    per-partition path and plans only the unsynchronized partition.
    
    Regression test `test_base_table_add_partition_mtmv`: an MV with two
    synced partitions, then the base table adds a third one. Asserts
    `NeedRefreshPartitions=["p_3"]`, `RefreshMode=PARTIAL`, that the new
    partition carries its rows, and that a following refresh reports
    `NOT_REFRESH`. `test_excluded_trigger_table_mtmv`,
    `test_multi_level_mtmv` and `test_partition_refresh_mtmv` pass as well.
    
    ### Related PR
    
    - apache/doris#64041 - introduced the refresh baseline check that this
    PR narrows.
---
 .../main/java/org/apache/doris/catalog/MTMV.java   | 10 +--
 .../apache/doris/job/extensions/mtmv/MTMVTask.java |  8 ++-
 .../java/org/apache/doris/mtmv/MTMVTaskTest.java   | 56 +++++++++++++++--
 .../test/java/org/apache/doris/mtmv/MTMVTest.java  | 24 ++++++++
 .../mtmv_p0/test_base_table_add_partition_mtmv.out | 17 +++++
 .../test_base_table_add_partition_mtmv.groovy      | 72 ++++++++++++++++++++++
 6 files changed, 175 insertions(+), 12 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java
index 82955035282..5bec54bdf0b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java
@@ -455,12 +455,14 @@ public class MTMV extends OlapTable {
         return refreshSnapshot;
     }
 
-    public boolean hasCompleteRefreshSnapshot() {
-        Set<String> partitionNames = getPartitionNames();
+    public boolean hasRefreshSnapshot() {
         readMvLock();
         try {
-            // A refresh baseline is complete only when every current MV 
partition has a snapshot.
-            return 
refreshSnapshot.getPartitionSnapshots().keySet().containsAll(partitionNames);
+            // The baseline is invalidated only when the snapshot map is 
emptied as a whole, which is what
+            // ALTER excluded_trigger_tables and a status change do. A newly 
added MV partition legitimately
+            // has no snapshot yet, and must not turn a single-partition 
change into a full refresh: the
+            // per-partition comparison decides for it instead.
+            return refreshSnapshot != null && 
!MapUtils.isEmpty(refreshSnapshot.getPartitionSnapshots());
         } finally {
             readMvUnlock();
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java 
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java
index f3dbabf97c9..7561042683f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java
@@ -688,9 +688,11 @@ public class MTMVTask extends AbstractTask {
         if (mtmv.getRefreshInfo().getRefreshMethod() == 
RefreshMethod.COMPLETE) {
             return Lists.newArrayList(mtmv.getPartitionNames());
         }
-        // An incomplete baseline cannot be checked by isMTMVSync, because the 
current exclude rules may
-        // skip the changed base tables and incorrectly mark the MV as fresh. 
Rebuild it with a full refresh.
-        if (!mtmv.hasCompleteRefreshSnapshot()) {
+        // A baseline that was invalidated as a whole cannot be checked by 
isMTMVSync, because the current
+        // exclude rules may skip the changed base tables and incorrectly mark 
the MV as fresh. Rebuild it
+        // with a full refresh. A partition that partition sync has just added 
is not such a case: it has no
+        // snapshot yet but is still compared per partition below, so only the 
new partition gets refreshed.
+        if (!mtmv.hasRefreshSnapshot()) {
             return Lists.newArrayList(mtmv.getPartitionNames());
         }
         // check if data is fresh
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTaskTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTaskTest.java
index 8d96489bf93..e7cb5201536 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTaskTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTaskTest.java
@@ -105,7 +105,7 @@ public class MTMVTaskTest {
                 minTimes = 0;
                 result = RefreshMethod.COMPLETE;
 
-                mtmv.hasCompleteRefreshSnapshot();
+                mtmv.hasRefreshSnapshot();
                 minTimes = 0;
                 result = true;
             }
@@ -153,14 +153,14 @@ public class MTMVTaskTest {
     }
 
     @Test
-    public void 
testCalculateNeedRefreshPartitionsSystemIncompleteRefreshSnapshot() throws 
AnalysisException {
+    public void 
testCalculateNeedRefreshPartitionsSystemInvalidatedRefreshSnapshot() throws 
AnalysisException {
         new Expectations() {
             {
                 mtmvRefreshInfo.getRefreshMethod();
                 minTimes = 0;
                 result = RefreshMethod.AUTO;
 
-                mtmv.hasCompleteRefreshSnapshot();
+                mtmv.hasRefreshSnapshot();
                 minTimes = 0;
                 result = false;
             }
@@ -181,11 +181,11 @@ public class MTMVTaskTest {
     }
 
     @Test
-    public void 
testCalculateNeedRefreshPartitionsManualPartitionsIncompleteRefreshSnapshot()
+    public void 
testCalculateNeedRefreshPartitionsManualPartitionsInvalidatedRefreshSnapshot()
             throws AnalysisException {
         new Expectations() {
             {
-                mtmv.hasCompleteRefreshSnapshot();
+                mtmv.hasRefreshSnapshot();
                 minTimes = 0;
                 result = false;
             }
@@ -239,6 +239,52 @@ public class MTMVTaskTest {
         Assert.assertEquals(Lists.newArrayList(ptwoName), result);
     }
 
+    @Test
+    public void 
testCalculateNeedRefreshPartitionsKeepsSyncedPartitionsWhenOneHasNoSnapshot()
+            throws AnalysisException {
+        // Partition sync adds an MV partition without a snapshot whenever its 
base partition appears, and
+        // REFRESH MATERIALIZED VIEW ... AUTO reaches this method in the 
MANUAL trigger mode. The partitions
+        // that were refreshed before did keep their snapshots, so only the 
new partition may be planned:
+        // reading the added partition as a lost baseline would rebuild the 
whole MV.
+        new Expectations() {
+            {
+                mtmvRefreshInfo.getRefreshMethod();
+                minTimes = 0;
+                result = RefreshMethod.AUTO;
+
+                mtmv.hasRefreshSnapshot();
+                minTimes = 0;
+                result = true;
+
+                mtmvPartitionUtil
+                        .isMTMVSync((MTMVRefreshContext) any, 
(Set<BaseTableInfo>) any, (Set<TableName>) any);
+                minTimes = 0;
+                result = false;
+
+                mtmvPartitionUtil
+                        .getMTMVNeedRefreshPartitions((MTMVRefreshContext) 
any, (Set<BaseTableInfo>) any);
+                minTimes = 0;
+                result = Lists.newArrayList(ptwoName);
+            }
+        };
+        MTMVTaskContext context = new 
MTMVTaskContext(MTMVTaskTriggerMode.MANUAL, null, false, null);
+        MTMVTask task = new MTMVTask(mtmv, relation, context);
+        List<String> result = task.calculateNeedRefreshPartitions(null);
+
+        Assert.assertEquals(Lists.newArrayList(ptwoName), result);
+        new Verifications() {
+            {
+                // The narrowed baseline check is consulted and passes, and 
the comparison is then left
+                // to the per-partition path instead of being short-circuited 
into a full refresh.
+                mtmv.hasRefreshSnapshot();
+                times = 1;
+                mtmvPartitionUtil.isMTMVSync((MTMVRefreshContext) any, 
(Set<BaseTableInfo>) any,
+                        (Set<TableName>) any);
+                times = 1;
+            }
+        };
+    }
+
     @Test
     public void testTaskSchemaContainsComputeGroup() {
         Column lastColumn = MTMVTask.SCHEMA.get(MTMVTask.SCHEMA.size() - 1);
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTest.java
index b25e84c3ffe..7499f1cd89b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVTest.java
@@ -210,6 +210,30 @@ public class MTMVTest {
         
Assert.assertTrue(mtmv.getRefreshSnapshot().getPartitionSnapshots().isEmpty());
     }
 
+    @Test
+    public void testHasRefreshSnapshotIgnoresPartitionsWithoutSnapshot() {
+        // A partition that partition sync just added has no snapshot yet. The 
baseline is still there as
+        // long as another partition has one, otherwise that single new 
partition forces a full refresh.
+        MTMV mtmv = new MTMV() {
+            @Override
+            public Set<String> getPartitionNames() {
+                return Sets.newHashSet("p1", "p2");
+            }
+        };
+        MTMVRefreshSnapshot refreshSnapshot = new MTMVRefreshSnapshot();
+        refreshSnapshot.getPartitionSnapshots().put("p1", new 
MTMVRefreshPartitionSnapshot());
+        mtmv.setRefreshSnapshot(refreshSnapshot);
+        Assert.assertTrue(mtmv.hasRefreshSnapshot());
+
+        // An emptied snapshot map is what ALTER excluded_trigger_tables 
leaves behind, and it must still
+        // be read as a lost baseline.
+        mtmv.setRefreshSnapshot(new MTMVRefreshSnapshot());
+        Assert.assertFalse(mtmv.hasRefreshSnapshot());
+
+        mtmv.setRefreshSnapshot(null);
+        Assert.assertFalse(mtmv.hasRefreshSnapshot());
+    }
+
     @Test
     public void testAlterMvPropertiesWithSameExcludedTriggerTables() {
         Map<String, String> mvProperties = Maps.newHashMap();
diff --git 
a/regression-test/data/mtmv_p0/test_base_table_add_partition_mtmv.out 
b/regression-test/data/mtmv_p0/test_base_table_add_partition_mtmv.out
new file mode 100644
index 00000000000..f3f3ec2051c
--- /dev/null
+++ b/regression-test/data/mtmv_p0/test_base_table_add_partition_mtmv.out
@@ -0,0 +1,17 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !baseline --
+1      1
+
+-- !need_refresh_partitions --
+["p_3"]
+
+-- !refresh_mode --
+PARTIAL
+
+-- !after_add_partition --
+1      1
+3      3
+
+-- !refresh_mode_no_change --
+NOT_REFRESH
+
diff --git 
a/regression-test/suites/mtmv_p0/test_base_table_add_partition_mtmv.groovy 
b/regression-test/suites/mtmv_p0/test_base_table_add_partition_mtmv.groovy
new file mode 100644
index 00000000000..d74324f2ab4
--- /dev/null
+++ b/regression-test/suites/mtmv_p0/test_base_table_add_partition_mtmv.groovy
@@ -0,0 +1,72 @@
+// 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.
+
+suite("test_base_table_add_partition_mtmv", "mtmv") {
+    def tableName = "test_base_table_add_partition_mtmv_table"
+    def mvName = "test_base_table_add_partition_mtmv_mv"
+    sql """drop table if exists `${tableName}`"""
+    sql """drop materialized view if exists ${mvName}"""
+
+    sql """
+        CREATE TABLE `${tableName}` (
+            k1 int,
+            k2 int
+        )
+        PARTITION BY LIST(`k1`)
+        (
+            PARTITION `p1` VALUES IN ('1'),
+            PARTITION `p2` VALUES IN ('2')
+        )
+        DISTRIBUTED BY HASH(k1) BUCKETS 2
+        PROPERTIES (
+            "replication_num" = "1"
+        );
+        """
+    sql """insert into ${tableName} values(1,1);"""
+
+    sql """
+        CREATE MATERIALIZED VIEW ${mvName}
+        BUILD DEFERRED REFRESH AUTO ON MANUAL
+        partition by(k1)
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS
+        SELECT * FROM ${tableName};
+        """
+    sql """REFRESH MATERIALIZED VIEW ${mvName} AUTO"""
+    waitingMTMVTaskFinishedByMvName(mvName)
+    order_qt_baseline "select * from ${mvName}"
+
+    // A new base partition makes the refresh add the matching MV partition, 
which has no snapshot yet.
+    // Only that partition may be refreshed: reading it as a lost refresh 
baseline would rebuild every
+    // partition of the MV, including the partitions that are already in sync.
+    sql """alter table ${tableName} add partition p3 values in ('3');"""
+    sql """insert into ${tableName} values(3,3);"""
+    sql """REFRESH MATERIALIZED VIEW ${mvName} AUTO"""
+    waitingMTMVTaskFinishedByMvName(mvName)
+    order_qt_need_refresh_partitions "select NeedRefreshPartitions from 
tasks('type'='mv') where MvName='${mvName}' order by CreateTime desc limit 1"
+    order_qt_refresh_mode "select RefreshMode from tasks('type'='mv') where 
MvName='${mvName}' order by CreateTime desc limit 1"
+    order_qt_after_add_partition "select * from ${mvName}"
+
+    // The added partition is in sync now, so a refresh without base table 
changes has nothing to do.
+    sql """REFRESH MATERIALIZED VIEW ${mvName} AUTO"""
+    waitingMTMVTaskFinishedByMvName(mvName)
+    order_qt_refresh_mode_no_change "select RefreshMode from 
tasks('type'='mv') where MvName='${mvName}' order by CreateTime desc limit 1"
+
+    sql """drop materialized view if exists ${mvName}"""
+    sql """drop table if exists `${tableName}`"""
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to