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

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


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new ddc988bc458 branch-4.2: [fix](fe) Keep deleted rows out of the 
simple-agg MIN/MAX cache #68990 (#69179)
ddc988bc458 is described below

commit ddc988bc45847572c4721d6758017415db34e7a8
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Oct 20 15:09:05 2026 +0800

    branch-4.2: [fix](fe) Keep deleted rows out of the simple-agg MIN/MAX cache 
#68990 (#69179)
    
    Cherry-picked from #68990
    
    Co-authored-by: minghong <[email protected]>
---
 .../doris/nereids/stats/SimpleAggCacheMgr.java     |  78 +++++++++++++---
 .../min_max_after_delete.out                       |   7 ++
 .../min_max_after_delete.groovy                    | 103 +++++++++++++++++++++
 3 files changed, 174 insertions(+), 14 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/SimpleAggCacheMgr.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/SimpleAggCacheMgr.java
index 1b67a7cd35d..7fe378992cb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/SimpleAggCacheMgr.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/SimpleAggCacheMgr.java
@@ -17,13 +17,18 @@
 
 package org.apache.doris.nereids.stats;
 
+import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.TableIf;
+import org.apache.doris.cloud.qe.ComputeGroupException;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.ThreadPoolManager;
 import org.apache.doris.qe.AutoCloseConnectContext;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
 import org.apache.doris.qe.StmtExecutor;
 import org.apache.doris.statistics.ResultRow;
 import org.apache.doris.statistics.util.StatisticsUtil;
@@ -404,6 +409,63 @@ public class SimpleAggCacheMgr {
         return "SELECT count(*) FROM " + fullTable;
     }
 
+    /**
+     * Builds the session that the internal SQL of this cache runs on.
+     *
+     * <p>Deliberately not {@code StatisticsUtil.buildConnectContext()}: that 
one is shaped for
+     * statistics collection, which only needs an approximate MIN/MAX and 
therefore turns on
+     * {@code force_pushdown_zonemap_minmax}. With that on, the BE answers a 
pushed-down MIN/MAX from
+     * the segment zone maps even when a delete predicate removed rows, and a 
zone map still covers
+     * what that predicate filters out. The cache would then store a deleted 
row's bound as the exact
+     * value and hand it to {@code RewriteSimpleAggToConstantRule}.
+     *
+     * <p>So this builder starts from a default {@link SessionVariable} and 
sets only what the query
+     * itself needs. Nothing here may trade accuracy for speed.
+     */
+    private static AutoCloseConnectContext buildConnectContext() {
+        ConnectContext connectContext = new ConnectContext();
+        connectContext.getState().setInternal(true);
+        connectContext.setEnv(Env.getCurrentEnv());
+        connectContext.setCurrentUserIdentity(UserIdentity.ADMIN);
+        connectContext.setStartTime();
+        // The internal SQL names its table fully qualified; this only gives 
the context a database.
+        connectContext.setDatabase(FeConstants.INTERNAL_DB_NAME);
+        SessionVariable sessionVariable = connectContext.getSessionVariable();
+        // A load is one aggregate over one table, so a single pipeline task 
is enough.
+        sessionVariable.parallelPipelineTaskNum = 1;
+        // Disable our own rule to prevent infinite recursion:
+        // this internal SQL goes through Nereids and would otherwise trigger
+        // RewriteSimpleAggToConstantRule again.
+        
sessionVariable.setDisableNereidsRules("REWRITE_SIMPLE_AGG_TO_CONSTANT");
+        // Statistics collection turns this on to accept approximate bounds. 
This cache promises
+        // exact ones, so a delete predicate has to keep MIN/MAX off the zone 
maps. COUNT keeps the
+        // BE's delete-predicate guard whether the switch is on or off.
+        sessionVariable.forcePushDownZonemapMinMax = false;
+        // The cache exists to answer from the table's current visible 
version. A result cache keyed
+        // on a session-variable signature must neither answer this query nor 
be filled by it.
+        sessionVariable.enableSqlCache = false;
+        sessionVariable.enableQueryCache = false;
+        // Read the base table: a materialized view answers from its own 
snapshot.
+        sessionVariable.enableMaterializedViewRewrite = false;
+        // A background scan of a whole column must not evict the pages user 
queries are reading, and
+        // this query's result outlives a user query by far once it is cached 
in FE, so it reads
+        // storage directly instead of through a block cache. disableFileCache 
only takes effect in
+        // cloud mode; elsewhere the block file cache is off regardless.
+        sessionVariable.enablePageCache = false;
+        sessionVariable.disableFileCache = true;
+        if (Config.isCloudMode()) {
+            try {
+                connectContext.getCloudCluster();
+            } catch (ComputeGroupException e) {
+                // Best effort, the way statistics collection treats it: the 
planner resolves the
+                // compute group again if this fails, and a load that cannot 
run costs the rewrite
+                // rather than correctness.
+                LOG.warn("failed to resolve the compute group of an internal 
min/max query", e);
+            }
+        }
+        return new AutoCloseConnectContext(connectContext);
+    }
+
     /**
      * Async cache loader that issues internal SQL queries to compute exact 
min/max.
      */
@@ -454,13 +516,7 @@ public class SimpleAggCacheMgr {
             String sql = genMinMaxSql(qualifiers, column.getName());
 
             List<ResultRow> rows;
-            try (AutoCloseConnectContext r = 
StatisticsUtil.buildConnectContext(false)) {
-                r.connectContext.getSessionVariable().setPipelineTaskNum("1");
-                // Disable our own rule to prevent infinite recursion:
-                // this internal SQL goes through Nereids and would otherwise 
trigger
-                // RewriteSimpleAggToConstantRule again.
-                r.connectContext.getSessionVariable().setDisableNereidsRules(
-                        "REWRITE_SIMPLE_AGG_TO_CONSTANT");
+            try (AutoCloseConnectContext r = buildConnectContext()) {
                 StmtExecutor stmtExecutor = new StmtExecutor(r.connectContext, 
sql);
                 rows = stmtExecutor.executeInternalQuery();
             }
@@ -523,13 +579,7 @@ public class SimpleAggCacheMgr {
             String sql = genCountSql(qualifiers);
 
             List<ResultRow> rows;
-            try (AutoCloseConnectContext r = 
StatisticsUtil.buildConnectContext(false)) {
-                r.connectContext.getSessionVariable().setPipelineTaskNum("1");
-                // Disable our own rule to prevent infinite recursion:
-                // this internal SQL goes through Nereids and would otherwise 
trigger
-                // RewriteSimpleAggToConstantRule again.
-                r.connectContext.getSessionVariable().setDisableNereidsRules(
-                        "REWRITE_SIMPLE_AGG_TO_CONSTANT");
+            try (AutoCloseConnectContext r = buildConnectContext()) {
                 StmtExecutor stmtExecutor = new StmtExecutor(r.connectContext, 
sql);
                 rows = stmtExecutor.executeInternalQuery();
             }
diff --git 
a/regression-test/data/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.out
 
b/regression-test/data/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.out
new file mode 100644
index 00000000000..f857b0d4607
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.out
@@ -0,0 +1,7 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !min_max_after_delete --
+2022-04-27T16:00:33    2022-04-27T16:00:58     1       2
+
+-- !min_max_after_delete_rule_off --
+2022-04-27T16:00:33    2022-04-27T16:00:58     1       2
+
diff --git 
a/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.groovy
 
b/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.groovy
new file mode 100644
index 00000000000..035f2cb34ed
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/min_max_after_delete.groovy
@@ -0,0 +1,103 @@
+// 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.
+
+// A no-filter MIN/MAX is rewritten to a constant from SimpleAggCacheMgr. That 
cache runs its
+// internal SQL on the statistics connect context, which forces MIN/MAX onto 
the segment zone maps.
+// A zone map still covers the rows a delete predicate filters out, so the 
cache used to store the
+// max of a row that had been DELETEd and the rewrite handed that value back 
to the user.
+suite("min_max_after_delete") {
+    // The rewrite is only reached when the plan is built, so the SQL result 
cache would answer the
+    // later queries without ever consulting SimpleAggCacheMgr.
+    sql "SET enable_sql_cache=false"
+
+    sql "DROP DATABASE IF EXISTS test_min_max_after_delete"
+    sql "CREATE DATABASE test_min_max_after_delete"
+    sql "USE test_min_max_after_delete"
+
+    // disable_auto_compaction is what preserves the fault this test is about. 
The cached bound is
+    // only wrong while the deleted row is still on disk behind its delete 
predicate, so that the
+    // segment zone map still holds it. Auto compaction is on by default, and 
a DELETE alone is
+    // enough to make cumulative compaction merge the preceding rowsets and 
base compaction apply
+    // the predicate. If that wins the race against the async cache load, the 
unfixed code answers
+    // correctly too and this test would pass without covering anything.
+    sql """
+        CREATE TABLE dup_min_max_deleted (
+            k VARCHAR(20) NULL,
+            d DATETIME NULL,
+            n BIGINT NULL
+        ) DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES(
+            'replication_num' = '1',
+            'disable_auto_compaction' = 'true'
+        )
+    """
+
+    sql """INSERT INTO dup_min_max_deleted VALUES ('keep_a', '2022-04-27 
16:00:33', 1)"""
+    sql """INSERT INTO dup_min_max_deleted VALUES ('keep_b', '2022-04-27 
16:00:58', 2)"""
+    // Holds both the max datetime and the max integer, and is deleted right 
below. DELETE leaves it
+    // as a delete predicate, so its rows are still on disk behind that 
predicate.
+    sql """INSERT INTO dup_min_max_deleted VALUES ('deleted_row', '2024-01-01 
00:00:00', 100)"""
+    sql """DELETE FROM dup_min_max_deleted WHERE k = 'deleted_row'"""
+
+    // ===================================================================
+    // Warm up the SimpleAggCacheMgr async cache.
+    //
+    // The first call to getStats() only triggers an async load. Poll until
+    // explain shows "constant exprs", which proves the cache entry -- the
+    // one that used to hold the deleted row's bound -- is in place.
+    //
+    // The query must push down MIN/MAX alone. Adding count(*) would make it
+    // a MIX push-down, which the BE keeps behind its delete-predicate guard,
+    // and the bug would not be exercised.
+    // ===================================================================
+    sql "SELECT min(d), max(d) FROM dup_min_max_deleted"
+    sql "SELECT min(n), max(n) FROM dup_min_max_deleted"
+
+    def minMaxSql = "SELECT min(d), max(d), min(n), max(n) FROM 
dup_min_max_deleted"
+    def cacheReady = false
+    for (int i = 0; i < 30; i++) {
+        def explainResult = sql "EXPLAIN ${minMaxSql}"
+        if (explainResult.toString().contains("constant exprs")) {
+            cacheReady = true
+            break
+        }
+        sleep(1000)
+    }
+    if (!cacheReady) {
+        if (isCloudMode()) {
+            logger.info("SimpleAggCacheMgr cache did not warm up within 30s in 
cloud mode, skip")
+            return
+        }
+        assertTrue(false, "SimpleAggCacheMgr cache did not warm up within 30 
seconds")
+    }
+
+    // The deleted row must not contribute to the answer, neither through the 
constant rewrite nor
+    // through the rows the BE reads. Both values below come from the two 
surviving rows.
+    explain {
+        sql("${minMaxSql}")
+        contains "constant exprs"
+    }
+    order_qt_min_max_after_delete """SELECT min(d), max(d), min(n), max(n) 
FROM dup_min_max_deleted"""
+
+    // A delete predicate has to keep the pushed-down MIN/MAX off the zone 
maps, so disabling the
+    // rewrite must agree with the rewritten answer above.
+    order_qt_min_max_after_delete_rule_off """
+        SELECT /*+ 
SET_VAR(disable_nereids_rules=REWRITE_SIMPLE_AGG_TO_CONSTANT) */
+            min(d), max(d), min(n), max(n) FROM dup_min_max_deleted
+    """
+}


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

Reply via email to