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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 00a946dc431 [branch-4.1][fix](load) only load source scanners update 
load counters (#65276)
00a946dc431 is described below

commit 00a946dc431cf01442850612393ee46144f40eb5
Author: Xin Liao <[email protected]>
AuthorDate: Tue Jul 7 13:41:33 2026 +0800

    [branch-4.1][fix](load) only load source scanners update load counters 
(#65276)
    
    Pick apache/doris#63781
---
 be/src/exec/scan/file_scanner.cpp                  | 13 +++
 be/src/exec/scan/file_scanner.h                    |  2 +
 be/src/exec/scan/scanner.cpp                       |  8 +-
 be/src/exec/scan/scanner.h                         |  7 ++
 ...scan_filtered_rows_not_pollute_load_counter.out | 14 ++++
 ...n_filtered_rows_not_pollute_load_counter.groovy | 94 ++++++++++++++++++++++
 6 files changed, 135 insertions(+), 3 deletions(-)

diff --git a/be/src/exec/scan/file_scanner.cpp 
b/be/src/exec/scan/file_scanner.cpp
index fdc53fc8231..e6cd425bd58 100644
--- a/be/src/exec/scan/file_scanner.cpp
+++ b/be/src/exec/scan/file_scanner.cpp
@@ -2009,6 +2009,19 @@ void FileScanner::update_realtime_counters() {
     _last_bytes_read_from_remote = 
_file_cache_statistics->bytes_read_from_remote;
 }
 
+bool FileScanner::_should_update_load_counters() const {
+    if (_is_load) {
+        return true;
+    }
+    // TVF based loads (e.g. http_stream, group commit relay) plan the load 
source as a
+    // tvf query scan without src tuple desc, so _is_load is false. But rows 
filtered by
+    // the load's WHERE clause still need to be reported as unselected rows. 
FILE_STREAM
+    // is only reachable from such load entries, never from normal queries, so 
use it to
+    // identify these scanners.
+    return (_params->__isset.file_type && _params->file_type == 
TFileType::FILE_STREAM) ||
+           (_current_range.__isset.file_type && _current_range.file_type == 
TFileType::FILE_STREAM);
+}
+
 void FileScanner::_collect_profile_before_close() {
     Scanner::_collect_profile_before_close();
     if (config::enable_file_cache && _state->query_options().enable_file_cache 
&&
diff --git a/be/src/exec/scan/file_scanner.h b/be/src/exec/scan/file_scanner.h
index 8a32d154695..833ca9419bb 100644
--- a/be/src/exec/scan/file_scanner.h
+++ b/be/src/exec/scan/file_scanner.h
@@ -112,6 +112,8 @@ protected:
 
     void _collect_profile_before_close() override;
 
+    bool _should_update_load_counters() const override;
+
     // fe will add skip_bitmap_col to _input_tuple_desc iff the target 
olaptable has skip_bitmap_col
     // and the current load is a flexible partial update
     bool _should_process_skip_bitmap_col() const { return _skip_bitmap_col_idx 
!= -1; }
diff --git a/be/src/exec/scan/scanner.cpp b/be/src/exec/scan/scanner.cpp
index 65dcb46c3b1..c8c959942d0 100644
--- a/be/src/exec/scan/scanner.cpp
+++ b/be/src/exec/scan/scanner.cpp
@@ -257,9 +257,11 @@ void Scanner::_collect_profile_before_close() {
     COUNTER_UPDATE(_local_state->_scan_cpu_timer, _scan_cpu_timer);
     COUNTER_UPDATE(_local_state->_rows_read_counter, _num_rows_read);
 
-    // Update stats for load
-    _state->update_num_rows_load_filtered(_counter.num_rows_filtered);
-    _state->update_num_rows_load_unselected(_counter.num_rows_unselected);
+    // Update stats for load. See _should_update_load_counters() for why this 
is gated.
+    if (_should_update_load_counters()) {
+        _state->update_num_rows_load_filtered(_counter.num_rows_filtered);
+        _state->update_num_rows_load_unselected(_counter.num_rows_unselected);
+    }
 }
 
 void Scanner::update_scan_cpu_timer() {
diff --git a/be/src/exec/scan/scanner.h b/be/src/exec/scan/scanner.h
index 241dd99107a..48f6f7ca3ab 100644
--- a/be/src/exec/scan/scanner.h
+++ b/be/src/exec/scan/scanner.h
@@ -117,6 +117,13 @@ protected:
     // Update the counters before closing this scanner
     virtual void _collect_profile_before_close();
 
+    // Whether rows filtered/unselected by this scanner should be reported to 
the load
+    // counters in RuntimeState. Only the scanner reading the load source data 
should
+    // report, otherwise rows filtered by query predicates (e.g. in INSERT 
INTO ... SELECT
+    // or DELETE FROM ... WHERE) would be mixed into load counters and make
+    // num_rows_load_success() negative.
+    virtual bool _should_update_load_counters() const { return _is_load; }
+
     // Filter the output block finally.
     Status _filter_output_block(Block* block);
 
diff --git 
a/regression-test/data/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.out
 
b/regression-test/data/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.out
new file mode 100644
index 00000000000..8a943e71beb
--- /dev/null
+++ 
b/regression-test/data/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.out
@@ -0,0 +1,14 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !insert_empty --
+0
+
+-- !insert_empty_profile --
+0
+
+-- !delete_noop --
+3
+
+-- !update_noop --
+1      1
+2      2
+3      3
diff --git 
a/regression-test/suites/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.groovy
 
b/regression-test/suites/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.groovy
new file mode 100644
index 00000000000..e0f4e587cef
--- /dev/null
+++ 
b/regression-test/suites/load_p0/insert/test_scan_filtered_rows_not_pollute_load_counter.groovy
@@ -0,0 +1,94 @@
+// 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_scan_filtered_rows_not_pollute_load_counter", "p0") {
+    // Rows filtered by scan predicates of a query must not be counted as load
+    // "unselected" rows. Otherwise loadedRows reported to FE becomes negative
+    // (total 0 - unselected N) and the insert fails with errors like
+    // "Insert has too many filtered data 0/-10 insert_max_filter_ratio is 
1.000000".
+    def srcTable = "test_scan_filter_load_counter_src"
+    def dstTable = "test_scan_filter_load_counter_dst"
+    def uniqTable = "test_scan_filter_load_counter_uniq"
+
+    sql """ DROP TABLE IF EXISTS ${srcTable} """
+    // Predicates on value columns of an AGGREGATE KEY table can neither be 
pushed
+    // down as column predicates nor as common expressions, so they are 
evaluated
+    // by the scanner conjuncts and counted into 
ScannerCounter.num_rows_unselected.
+    sql """
+        CREATE TABLE ${srcTable} (
+            k1 INT,
+            v1 INT REPLACE
+        ) AGGREGATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO ${srcTable} VALUES
+            (1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)
+    """
+
+    sql """ DROP TABLE IF EXISTS ${dstTable} """
+    sql """
+        CREATE TABLE ${dstTable} (
+            k1 INT
+        ) DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");
+    """
+
+    sql "set enable_insert_strict=false"
+    sql "set insert_max_filter_ratio=1"
+
+    // All 10 scanned rows are filtered inside the scanner; the insert must
+    // succeed as a no-op instead of failing the filter ratio check.
+    sql """ INSERT INTO ${dstTable} SELECT k1 FROM ${srcTable} WHERE v1 > 1000 
"""
+    qt_insert_empty "select count(*) from ${dstTable}"
+
+    // Same with profile enabled, which was the original trigger of this issue.
+    sql "set enable_profile=true"
+    sql """ INSERT INTO ${dstTable} SELECT k1 FROM ${srcTable} WHERE v1 > 1000 
"""
+    qt_insert_empty_profile "select count(*) from ${dstTable}"
+    sql "set enable_profile=false"
+
+    // DELETE ... WHERE EXISTS executes through the insert path (delete sign).
+    // A no-op delete whose source scan filters out all rows must succeed.
+    sql """ DROP TABLE IF EXISTS ${uniqTable} """
+    sql """
+        CREATE TABLE ${uniqTable} (
+            k1 INT,
+            v1 INT
+        ) UNIQUE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """ INSERT INTO ${uniqTable} VALUES (1,1),(2,2),(3,3) """
+    sql """
+        DELETE FROM ${uniqTable} t WHERE EXISTS (
+            SELECT 1 FROM ${srcTable} s WHERE s.k1 = t.k1 AND s.v1 > 1000
+        )
+    """
+    qt_delete_noop "select count(*) from ${uniqTable}"
+
+    // UPDATE also executes through the insert path; a no-op update whose
+    // subquery scan filters out all rows must succeed.
+    sql """
+        UPDATE ${uniqTable} SET v1 = 100 WHERE k1 IN (
+            SELECT k1 FROM ${srcTable} WHERE v1 > 1000
+        )
+    """
+    qt_update_noop "select * from ${uniqTable} order by k1"
+}


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

Reply via email to