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

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


The following commit(s) were added to refs/heads/4.1_performance by this push:
     new fd35abc4111 [opt](exec) optimize nullable predicate scan performance 
(#66765)
fd35abc4111 is described below

commit fd35abc41116692287db3769b2cfe430baf8ee58
Author: Mryange <[email protected]>
AuthorDate: Fri Aug 14 14:20:23 2026 +0800

    [opt](exec) optimize nullable predicate scan performance (#66765)
    
    [opt](exec) optimize nullable predicate scan performance
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: #66688
    
    Problem Summary:
    
    Nullable predicate scans repeatedly traverse all-zero null maps even
    when a decoded batch contains no NULL values. In addition,
    `Roaring::cardinality()` is recomputed for every scan batch although the
    row bitmap is unchanged after iterator initialization.
    
    Root cause: the segment iterator does not preserve the decoded batch's
    null state, and it queries row bitmap cardinality in the per-batch path.
    
    This PR:
    
    - vectorizes nullable comparisons by replacing short-circuit `&&` with
    bitwise `&`;
    - propagates `batch_has_null` from column decoding and evaluates
    null-free comparison batches on the nested column;
    - filters nested values directly for null-free output and creates an
    all-zero output null map;
    - caches the final row bitmap cardinality during lazy initialization.
    
    Unknown null states, batches containing NULL values, and unsupported
    predicates retain the original nullable path.
    
    Local manual tests used a 2-billion-row, single-tablet table with one
    scanner and profile/cache disabled. The nullable scans averaged 4.648s
    versus 6.417s in StarRocks for `>=`, 3.907s versus 4.127s for `BETWEEN`,
    and 2.314s versus 2.660s for equality. A four-scan `UNION ALL` query
    averaged 4.654s in Doris versus 5.587s in StarRocks.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [x] Manual test (details included above)
        - [ ] Regression test
        - [ ] Unit Test
    - Behavior changed:
        - [x] No.
    - Does this need documentation?
        - [x] No.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 be/src/storage/predicate/comparison_predicate.h |  4 +-
 be/src/storage/segment/segment_iterator.cpp     | 69 +++++++++++++++++++++----
 be/src/storage/segment/segment_iterator.h       | 17 +++---
 3 files changed, 72 insertions(+), 18 deletions(-)

diff --git a/be/src/storage/predicate/comparison_predicate.h 
b/be/src/storage/predicate/comparison_predicate.h
index 657fae0ec6d..ece23a14733 100644
--- a/be/src/storage/predicate/comparison_predicate.h
+++ b/be/src/storage/predicate/comparison_predicate.h
@@ -559,7 +559,7 @@ private:
         if constexpr (is_and) {
             for (uint16_t i = 0; i < size; i++) {
                 if constexpr (is_nullable) {
-                    flags[i] &= (uint8_t)(!null_map[i] && 
_operator(data_array[i], value));
+                    flags[i] &= (uint8_t)(!null_map[i] & 
_operator(data_array[i], value));
                 } else {
                     flags[i] &= (uint8_t)_operator(data_array[i], value);
                 }
@@ -567,7 +567,7 @@ private:
         } else {
             for (uint16_t i = 0; i < size; i++) {
                 if constexpr (is_nullable) {
-                    flags[i] = !null_map[i] && _operator(data_array[i], value);
+                    flags[i] = !null_map[i] & _operator(data_array[i], value);
                 } else {
                     flags[i] = _operator(data_array[i], value);
                 }
diff --git a/be/src/storage/segment/segment_iterator.cpp 
b/be/src/storage/segment/segment_iterator.cpp
index 3b299de1367..ab3c9de4ce5 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -526,6 +526,8 @@ Status SegmentIterator::_lazy_init(Block* block) {
 
     RETURN_IF_ERROR(_apply_ann_topn_predicate());
 
+    _row_bitmap_cardinality = _row_bitmap.cardinality();
+
     if (_opts.read_orderby_key_reverse) {
         _range_iter.reset(new BackwardBitmapRangeIterator(_row_bitmap));
     } else {
@@ -536,12 +538,12 @@ Status SegmentIterator::_lazy_init(Block* block) {
     // prediction) because the predictor may increase block_row_max on 
subsequent batches
     // up to this ceiling. Using the current (possibly reduced) 
_opts.block_row_max would
     // cause heap-buffer-overflow if a later prediction is larger.
-    auto nrows_reserve_limit =
-            std::min(_row_bitmap.cardinality(), 
uint64_t(_initial_block_row_max));
+    auto nrows_reserve_limit = std::min(_row_bitmap_cardinality, 
uint64_t(_initial_block_row_max));
     if (_lazy_materialization_read || _opts.record_rowids || 
_is_need_expr_eval) {
         _block_rowids.resize(_initial_block_row_max);
     }
     _current_return_columns.resize(_schema->columns().size());
+    _predicate_column_has_null.assign(_schema->columns().size(), true);
 
     for (size_t i = 0; i < _schema->column_ids().size(); i++) {
         ColumnId cid = _schema->column_ids()[i];
@@ -2100,6 +2102,20 @@ bool 
SegmentIterator::_can_evaluated_by_vectorized(std::shared_ptr<ColumnPredica
     }
 }
 
+bool SegmentIterator::_can_evaluate_without_null_map(const ColumnPredicate& 
predicate) const {
+    switch (predicate.type()) {
+    case PredicateType::EQ:
+    case PredicateType::NE:
+    case PredicateType::LE:
+    case PredicateType::LT:
+    case PredicateType::GE:
+    case PredicateType::GT:
+        return true;
+    default:
+        return false;
+    }
+}
+
 bool SegmentIterator::_prune_column(ColumnId cid, MutableColumnPtr& column, 
bool fill_defaults,
                                     size_t num_of_defaults) {
     if (_need_read_data(cid)) {
@@ -2275,6 +2291,7 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
             nrows_read > 0 ? _block_rowids[nrows_read - 1] : 0);
     for (auto cid : _predicate_column_ids) {
         auto& column = _current_return_columns[cid];
+        _predicate_column_has_null[cid] = true;
         VLOG_DEBUG << fmt::format("Reading column {}, col_name {}", cid,
                                   _schema->column(cid)->name());
         if (!_virtual_column_exprs.contains(cid)) {
@@ -2283,6 +2300,7 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
                 continue;
             }
             if (_prune_column(cid, column, true, nrows_read)) {
+                _predicate_column_has_null[cid] = false;
                 VLOG_DEBUG << fmt::format("Column {} is pruned. No need to 
read data.", cid);
                 continue;
             }
@@ -2304,6 +2322,7 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
 
         if (is_continuous) {
             size_t rows_read = nrows_read;
+            bool batch_has_null = true;
             _opts.stats->predicate_column_read_seek_num += 1;
             if (_opts.runtime_state && _opts.runtime_state->enable_profile()) {
                 SCOPED_RAW_TIMER(&_opts.stats->predicate_column_read_seek_ns);
@@ -2311,7 +2330,9 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
             } else {
                 
RETURN_IF_ERROR(_column_iterators[cid]->seek_to_ordinal(_block_rowids[0]));
             }
-            RETURN_IF_ERROR(_column_iterators[cid]->next_batch(&rows_read, 
column));
+            RETURN_IF_ERROR(
+                    _column_iterators[cid]->next_batch(&rows_read, column, 
&batch_has_null));
+            _predicate_column_has_null[cid] = batch_has_null;
             if (rows_read != nrows_read) {
                 return Status::Error<ErrorCode::INTERNAL_ERROR>("nrows({}) != 
rows_read({})",
                                                                 nrows_read, 
rows_read);
@@ -2319,6 +2340,8 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
         } else {
             const uint32_t batch_size = _range_iter->get_batch_size();
             uint32_t processed = 0;
+            bool column_has_null = false;
+            bool has_unknown_null_state = false;
             while (processed < nrows_read) {
                 uint32_t current_batch_size = std::min(batch_size, nrows_read 
- processed);
                 bool batch_continuous = (current_batch_size > 1) &&
@@ -2328,6 +2351,7 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
 
                 if (batch_continuous) {
                     size_t rows_read = current_batch_size;
+                    bool batch_has_null = true;
                     _opts.stats->predicate_column_read_seek_num += 1;
                     if (_opts.runtime_state && 
_opts.runtime_state->enable_profile()) {
                         
SCOPED_RAW_TIMER(&_opts.stats->predicate_column_read_seek_ns);
@@ -2337,7 +2361,9 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
                         RETURN_IF_ERROR(
                                 
_column_iterators[cid]->seek_to_ordinal(_block_rowids[processed]));
                     }
-                    
RETURN_IF_ERROR(_column_iterators[cid]->next_batch(&rows_read, column));
+                    
RETURN_IF_ERROR(_column_iterators[cid]->next_batch(&rows_read, column,
+                                                                       
&batch_has_null));
+                    column_has_null |= batch_has_null;
                     if (rows_read != current_batch_size) {
                         return Status::Error<ErrorCode::INTERNAL_ERROR>(
                                 "batch nrows({}) != rows_read({})", 
current_batch_size, rows_read);
@@ -2345,9 +2371,11 @@ Status SegmentIterator::_read_columns_by_index(uint32_t 
nrows_read_limit, uint16
                 } else {
                     RETURN_IF_ERROR(_column_iterators[cid]->read_by_rowids(
                             &_block_rowids[processed], current_batch_size, 
column));
+                    has_unknown_null_state = true;
                 }
                 processed += current_batch_size;
             }
+            _predicate_column_has_null[cid] = column_has_null || 
has_unknown_null_state;
         }
     }
 
@@ -2411,11 +2439,16 @@ uint16_t 
SegmentIterator::_evaluate_vectorization_predicate(uint16_t* sel_rowid_
         }
         auto column_id = pred->column_id();
         auto& column = _current_return_columns[column_id];
+        const auto* predicate_column = column.get();
+        if (column->is_nullable() && !_predicate_column_has_null[column_id] &&
+            _can_evaluate_without_null_map(*pred)) {
+            predicate_column = &assert_cast<const 
ColumnNullable&>(*column).get_nested_column();
+        }
         if (is_first) {
-            pred->evaluate_vec(*column, original_size, 
(bool*)_ret_flags.data());
+            pred->evaluate_vec(*predicate_column, original_size, 
(bool*)_ret_flags.data());
             is_first = false;
         } else {
-            pred->evaluate_and_vec(*column, original_size, 
(bool*)_ret_flags.data());
+            pred->evaluate_and_vec(*predicate_column, original_size, 
(bool*)_ret_flags.data());
         }
     }
 
@@ -2466,7 +2499,13 @@ uint16_t 
SegmentIterator::_evaluate_short_circuit_predicate(uint16_t* vec_sel_ro
     for (auto predicate : _short_cir_eval_predicate) {
         auto column_id = predicate->column_id();
         auto& short_cir_column = _current_return_columns[column_id];
-        selected_size = predicate->evaluate(*short_cir_column, 
vec_sel_rowid_idx, selected_size);
+        const auto* predicate_column = short_cir_column.get();
+        if (short_cir_column->is_nullable() && 
!_predicate_column_has_null[column_id] &&
+            _can_evaluate_without_null_map(*predicate)) {
+            predicate_column =
+                    &assert_cast<const 
ColumnNullable&>(*short_cir_column).get_nested_column();
+        }
+        selected_size = predicate->evaluate(*predicate_column, 
vec_sel_rowid_idx, selected_size);
     }
 
     _opts.stats->short_circuit_cond_input_rows += original_size;
@@ -2654,7 +2693,7 @@ Status SegmentIterator::_convert_to_expected_type(const 
std::vector<ColumnId>& c
 Status SegmentIterator::copy_column_data_by_selector(IColumn* input_col_ptr,
                                                      MutableColumnPtr& 
output_col,
                                                      uint16_t* sel_rowid_idx, 
uint16_t select_size,
-                                                     size_t batch_size) {
+                                                     size_t batch_size, bool 
input_has_null) {
     if (output_col->is_nullable() != input_col_ptr->is_nullable()) {
         LOG(WARNING) << "nullable mismatch for output_column: " << 
output_col->dump_structure()
                      << " input_column: " << input_col_ptr->dump_structure()
@@ -2662,6 +2701,18 @@ Status 
SegmentIterator::copy_column_data_by_selector(IColumn* input_col_ptr,
         return Status::RuntimeError("copy_column_data_by_selector nullable 
mismatch");
     }
     output_col->reserve(select_size);
+    if (input_col_ptr->is_nullable() && !input_has_null) {
+        const auto& input_nullable = assert_cast<const 
ColumnNullable&>(*input_col_ptr);
+        auto& output_nullable = assert_cast<ColumnNullable&>(*output_col);
+        auto* output_nested = output_nullable.get_nested_column_ptr().get();
+        auto* input_nested = 
const_cast<IColumn*>(&input_nullable.get_nested_column());
+        RETURN_IF_ERROR(
+                input_nested->filter_by_selector(sel_rowid_idx, select_size, 
output_nested));
+        auto& output_null_map = output_nullable.get_null_map_data();
+        DCHECK(output_null_map.empty());
+        output_null_map.resize_fill(select_size, 0);
+        return Status::OK();
+    }
     return input_col_ptr->filter_by_selector(sel_rowid_idx, select_size, 
output_col.get());
 }
 
@@ -2677,7 +2728,7 @@ Status SegmentIterator::_next_batch_internal(Block* 
block) {
 
     // If the row bitmap size is smaller than nrows_read_limit, there's no 
need to reserve that many column rows.
     uint32_t nrows_read_limit =
-            std::min(cast_set<uint32_t>(_row_bitmap.cardinality()), 
_opts.block_row_max);
+            std::min(cast_set<uint32_t>(_row_bitmap_cardinality), 
_opts.block_row_max);
     if (_can_opt_topn_reads()) {
         nrows_read_limit = std::min(static_cast<uint32_t>(_opts.topn_limit), 
nrows_read_limit);
     }
diff --git a/be/src/storage/segment/segment_iterator.h 
b/be/src/storage/segment/segment_iterator.h
index d8f61daeba3..f89f5f4470f 100644
--- a/be/src/storage/segment/segment_iterator.h
+++ b/be/src/storage/segment/segment_iterator.h
@@ -231,7 +231,7 @@ private:
 
     Status copy_column_data_by_selector(IColumn* input_col_ptr, 
MutableColumnPtr& output_col,
                                         uint16_t* sel_rowid_idx, uint16_t 
select_size,
-                                        size_t batch_size);
+                                        size_t batch_size, bool input_has_null 
= true);
 
     template <class Container>
     [[nodiscard]] Status _output_column_by_sel_idx(Block* block, const 
Container& column_ids,
@@ -253,24 +253,25 @@ private:
             if (storage_type && 
!storage_type->equals(*block->get_by_position(block_cid).type)) {
                 // Do additional cast
                 MutableColumnPtr tmp = storage_type->create_column();
-                
RETURN_IF_ERROR(copy_column_data_by_selector(_current_return_columns[cid].get(),
-                                                             tmp, 
sel_rowid_idx, select_size,
-                                                             
_opts.block_row_max));
+                RETURN_IF_ERROR(copy_column_data_by_selector(
+                        _current_return_columns[cid].get(), tmp, 
sel_rowid_idx, select_size,
+                        _opts.block_row_max, _predicate_column_has_null[cid]));
                 RETURN_IF_ERROR(variant_util::cast_column(
                         {tmp->get_ptr(), storage_type, ""}, 
block->get_by_position(block_cid).type,
                         &block->get_by_position(block_cid).column));
             } else {
                 auto output_column_guard = 
block->mutate_column_scoped(block_cid);
                 auto& output_column = output_column_guard.mutable_column();
-                
RETURN_IF_ERROR(copy_column_data_by_selector(_current_return_columns[cid].get(),
-                                                             output_column, 
sel_rowid_idx,
-                                                             select_size, 
_opts.block_row_max));
+                RETURN_IF_ERROR(copy_column_data_by_selector(
+                        _current_return_columns[cid].get(), output_column, 
sel_rowid_idx,
+                        select_size, _opts.block_row_max, 
_predicate_column_has_null[cid]));
             }
         }
         return Status::OK();
     }
 
     bool _can_evaluated_by_vectorized(std::shared_ptr<ColumnPredicate> 
predicate);
+    bool _can_evaluate_without_null_map(const ColumnPredicate& predicate) 
const;
 
     [[nodiscard]] Status _extract_common_expr_columns(const VExprSPtr& expr);
     // same with _extract_common_expr_columns, but only extract columns that 
can be used for index
@@ -354,6 +355,7 @@ private:
     std::vector<std::unique_ptr<IndexIterator>> _index_iterators;
     // after init(), `_row_bitmap` contains all rowid to scan
     roaring::Roaring _row_bitmap;
+    uint64_t _row_bitmap_cardinality = 0;
     // an iterator for `_row_bitmap` that can be used to extract row range to 
scan
     std::unique_ptr<BitmapRangeIterator> _range_iter;
     // the next rowid to read
@@ -381,6 +383,7 @@ private:
     std::map<uint32_t, bool> _need_read_data_indices;
     std::vector<bool> _is_common_expr_column;
     MutableColumns _current_return_columns;
+    std::vector<bool> _predicate_column_has_null;
     std::vector<std::shared_ptr<ColumnPredicate>> _pre_eval_block_predicate;
     std::vector<std::shared_ptr<ColumnPredicate>> _short_cir_eval_predicate;
     std::vector<uint32_t> _delete_range_column_ids;


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

Reply via email to