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]