This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 73032987277 [improvement](be) Optimize Parquet predicate filtering in
format v2 (#65369)
73032987277 is described below
commit 730329872770d909d1c1e67431d2c70be97a7eab
Author: Gabriel <[email protected]>
AuthorDate: Fri Jul 10 13:22:26 2026 +0800
[improvement](be) Optimize Parquet predicate filtering in format v2 (#65369)
Problem Summary: The format_v2 Parquet reader previously materialized all
predicate columns before evaluating row-level predicates, so later predicate
columns could not reuse rows rejected by earlier single-column predicates. This
PR implements the StarRocks-style round-by-round predicate-column read path for
Doris format_v2: deterministic single-column conjuncts are scheduled with their
predicate columns, evaluated immediately after each column is read, and later
predicate columns ar [...]
The PR also adds row-level dictionary predicate filtering for
dictionary-encoded Parquet columns in the v2 path. Dictionary-capable predicate
children are evaluated against dictionary entries first, the data-page reader
filters by dictionary id, and residual conjuncts keep the normal row-level
expression path. Compound AND predicates are split so dictionary-covered
children are not re-evaluated after the dictionary prefilter, while
non-dictionary residual children still run on survivi [...]
For correctness, volatile or non-deterministic predicates stay on the old
full-batch path. Expressions such as random/rand, random_bytes, uuid, and
uuid_numeric are marked non-deterministic at the vectorized expression layer;
if any pushed conjunct is non-deterministic, the round-by-round schedule is
disabled for that batch so stateful functions see the same full input stream as
before this optimization.
---
be/src/exprs/vcast_expr.h | 4 +
be/src/exprs/vectorized_fn_call.cpp | 13 +
be/src/exprs/vectorized_fn_call.h | 2 +
be/src/exprs/vexpr.h | 11 +
be/src/format_v2/parquet/parquet_file_context.cpp | 7 +
be/src/format_v2/parquet/parquet_file_context.h | 4 +
be/src/format_v2/parquet/parquet_profile.cpp | 25 +
be/src/format_v2/parquet/parquet_profile.h | 21 +-
be/src/format_v2/parquet/parquet_scan.cpp | 707 ++++++++++++++++++++-
be/src/format_v2/parquet/parquet_scan.h | 28 +-
be/src/format_v2/parquet/parquet_statistics.cpp | 41 +-
be/src/format_v2/parquet/parquet_statistics.h | 32 +
be/src/format_v2/parquet/reader/column_reader.cpp | 51 +-
be/src/format_v2/parquet/reader/column_reader.h | 13 +-
.../parquet/reader/parquet_leaf_reader.cpp | 63 ++
.../format_v2/parquet/reader/parquet_leaf_reader.h | 3 +
.../parquet/reader/scalar_column_reader.cpp | 204 ++++++
.../parquet/reader/scalar_column_reader.h | 16 +
be/test/exprs/try_cast_expr_test.cpp | 10 +-
be/test/format_v2/parquet/parquet_reader_test.cpp | 589 +++++++++++++++++
be/test/format_v2/parquet/parquet_scan_test.cpp | 202 ++++++
.../hive/test_parquet_lazy_mat_profile.groovy | 36 ++
22 files changed, 2006 insertions(+), 76 deletions(-)
diff --git a/be/src/exprs/vcast_expr.h b/be/src/exprs/vcast_expr.h
index f0f3ead95d5..4ac5539ff28 100644
--- a/be/src/exprs/vcast_expr.h
+++ b/be/src/exprs/vcast_expr.h
@@ -55,6 +55,7 @@ public:
const std::string& expr_name() const override;
std::string debug_string() const override;
const DataTypePtr& get_target_type() const;
+ bool is_safe_to_execute_on_selected_rows() const override { return false; }
virtual std::string cast_name() const { return "CAST"; }
Status clone_node(VExprSPtr* cloned_expr) const override {
@@ -99,6 +100,9 @@ public:
size_t count, ColumnPtr& result_column) const
override;
~TryCastExpr() override = default;
std::string cast_name() const override { return "TRY CAST"; }
+ bool is_safe_to_execute_on_selected_rows() const override {
+ return VExpr::is_safe_to_execute_on_selected_rows();
+ }
Status clone_node(VExprSPtr* cloned_expr) const override {
DORIS_CHECK(cloned_expr != nullptr);
auto node = clone_texpr_node();
diff --git a/be/src/exprs/vectorized_fn_call.cpp
b/be/src/exprs/vectorized_fn_call.cpp
index a36f0da49b6..b6e5c13d111 100644
--- a/be/src/exprs/vectorized_fn_call.cpp
+++ b/be/src/exprs/vectorized_fn_call.cpp
@@ -25,6 +25,7 @@
#include <memory>
#include <ostream>
+#include <set>
#include "common/config.h"
#include "common/exception.h"
@@ -378,6 +379,18 @@ bool VectorizedFnCall::can_push_down_to_index() const {
return _function->can_push_down_to_index();
}
+bool VectorizedFnCall::is_deterministic() const {
+ static const std::set<std::string> NON_DETERMINISTIC_FUNCTIONS = {
+ "random", "rand", "random_bytes", "uuid", "uuid_numeric"};
+ return !NON_DETERMINISTIC_FUNCTIONS.contains(_function_name) &&
VExpr::is_deterministic();
+}
+
+bool VectorizedFnCall::is_safe_to_execute_on_selected_rows() const {
+ static const std::set<std::string> ERROR_PRESERVING_FUNCTIONS =
{"assert_true"};
+ return !ERROR_PRESERVING_FUNCTIONS.contains(_function_name) &&
+ VExpr::is_safe_to_execute_on_selected_rows();
+}
+
bool VectorizedFnCall::equals(const VExpr& other) {
const auto* other_ptr = dynamic_cast<const VectorizedFnCall*>(&other);
if (!other_ptr) {
diff --git a/be/src/exprs/vectorized_fn_call.h
b/be/src/exprs/vectorized_fn_call.h
index 67d5dbe158d..8343242ce31 100644
--- a/be/src/exprs/vectorized_fn_call.h
+++ b/be/src/exprs/vectorized_fn_call.h
@@ -77,6 +77,8 @@ public:
std::any_of(_children.begin(), _children.end(),
[](VExprSPtr child) { return child->is_blockable();
});
}
+ bool is_deterministic() const override;
+ bool is_safe_to_execute_on_selected_rows() const override;
bool is_constant() const override {
if (!_function->is_use_default_implementation_for_constants() ||
// udf function with no argument, can't sure it's must return
const column
diff --git a/be/src/exprs/vexpr.h b/be/src/exprs/vexpr.h
index 209de7b431f..f54af6f7819 100644
--- a/be/src/exprs/vexpr.h
+++ b/be/src/exprs/vexpr.h
@@ -180,6 +180,17 @@ public:
[](VExprSPtr child) { return child->is_blockable();
});
}
+ [[nodiscard]] virtual bool is_deterministic() const {
+ return std::ranges::all_of(
+ _children, [](const VExprSPtr& child) { return
child->is_deterministic(); });
+ }
+
+ [[nodiscard]] virtual bool is_safe_to_execute_on_selected_rows() const {
+ return is_deterministic() && std::ranges::all_of(_children, [](const
VExprSPtr& child) {
+ return child->is_safe_to_execute_on_selected_rows();
+ });
+ }
+
// execute current expr with inverted index to filter block. Given a
roaring bitmap of match rows
virtual Status evaluate_inverted_index(VExprContext* context, uint32_t
segment_num_rows) {
return Status::OK();
diff --git a/be/src/format_v2/parquet/parquet_file_context.cpp
b/be/src/format_v2/parquet/parquet_file_context.cpp
index 1df3d7b353d..d80dc58181d 100644
--- a/be/src/format_v2/parquet/parquet_file_context.cpp
+++ b/be/src/format_v2/parquet/parquet_file_context.cpp
@@ -353,6 +353,8 @@ public:
return true;
}
+ void reset_random_access_ranges() { reset_active_file_reader(); }
+
ParquetPageCacheStats page_cache_stats() const {
std::lock_guard lock(_page_cache_mutex);
return _page_cache_stats;
@@ -584,6 +586,11 @@ bool ParquetFileContext::set_random_access_ranges(const
std::vector<ParquetPageC
->set_random_access_ranges(ranges, avg_io_size, profile,
merge_read_slice_size);
}
+void ParquetFileContext::reset_random_access_ranges() {
+ DORIS_CHECK(arrow_file != nullptr);
+
static_cast<DorisRandomAccessFile*>(arrow_file.get())->reset_random_access_ranges();
+}
+
ParquetPageCacheStats ParquetFileContext::page_cache_stats() const {
if (arrow_file == nullptr) {
return {};
diff --git a/be/src/format_v2/parquet/parquet_file_context.h
b/be/src/format_v2/parquet/parquet_file_context.h
index e75293f470f..0dca5224495 100644
--- a/be/src/format_v2/parquet/parquet_file_context.h
+++ b/be/src/format_v2/parquet/parquet_file_context.h
@@ -129,6 +129,10 @@ struct ParquetFileContext {
bool set_random_access_ranges(const std::vector<ParquetPageCacheRange>&
ranges,
size_t avg_io_size, RuntimeProfile* profile,
int64_t merge_read_slice_size);
+ // Restore Arrow ReadAt() to the base Doris file reader and flush any
active merge-reader
+ // counters. Row-group setup uses this before dictionary-page probes,
because those probes are
+ // a separate pass over the column chunk from the later Arrow RecordReader
data-page stream.
+ void reset_random_access_ranges();
ParquetPageCacheStats page_cache_stats() const;
Status close();
};
diff --git a/be/src/format_v2/parquet/parquet_profile.cpp
b/be/src/format_v2/parquet/parquet_profile.cpp
index 4bf7af687f2..d41ff295ec4 100644
--- a/be/src/format_v2/parquet/parquet_profile.cpp
+++ b/be/src/format_v2/parquet/parquet_profile.cpp
@@ -139,6 +139,22 @@ void ParquetProfile::init(RuntimeProfile* profile) {
ADD_CHILD_TIMER_WITH_LEVEL(profile, "PredicateFilterTime",
parquet_profile, 1);
dict_filter_rewrite_time =
ADD_CHILD_TIMER_WITH_LEVEL(profile, "DictFilterRewriteTime",
parquet_profile, 1);
+ dict_filter_expr_rewrite_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(profile, "DictFilterExprRewriteTime",
parquet_profile, 1);
+ dict_filter_read_dict_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(profile, "DictFilterReadDictTime",
parquet_profile, 1);
+ dict_filter_build_time =
+ ADD_CHILD_TIMER_WITH_LEVEL(profile, "DictFilterBuildTime",
parquet_profile, 1);
+ dict_filter_candidate_columns = ADD_CHILD_COUNTER_WITH_LEVEL(
+ profile, "DictFilterCandidateColumns", TUnit::UNIT,
parquet_profile, 1);
+ dict_filter_columns = ADD_CHILD_COUNTER_WITH_LEVEL(profile,
"DictFilterColumns", TUnit::UNIT,
+ parquet_profile, 1);
+ dict_filter_unsupported_columns = ADD_CHILD_COUNTER_WITH_LEVEL(
+ profile, "DictFilterUnsupportedColumns", TUnit::UNIT,
parquet_profile, 1);
+ dict_filter_read_failures = ADD_CHILD_COUNTER_WITH_LEVEL(profile,
"DictFilterReadFailures",
+ TUnit::UNIT,
parquet_profile, 1);
+ rows_filtered_by_dict_filter = ADD_CHILD_COUNTER_WITH_LEVEL(profile,
"RowsFilteredByDictFilter",
+ TUnit::UNIT,
parquet_profile, 1);
convert_time = ADD_CHILD_TIMER_WITH_LEVEL(profile, "ConvertTime",
parquet_profile, 1);
bloom_filter_read_time =
ADD_CHILD_TIMER_WITH_LEVEL(profile, "BloomFilterReadTime",
parquet_profile, 1);
@@ -197,6 +213,15 @@ ParquetScanProfile ParquetProfile::scan_profile() const {
.range_gap_skipped_rows = range_gap_skipped_rows,
.column_read_time = column_read_time,
.predicate_filter_time = predicate_filter_time,
+ .dict_filter_rewrite_time = dict_filter_rewrite_time,
+ .dict_filter_expr_rewrite_time = dict_filter_expr_rewrite_time,
+ .dict_filter_read_dict_time = dict_filter_read_dict_time,
+ .dict_filter_build_time = dict_filter_build_time,
+ .dict_filter_candidate_columns = dict_filter_candidate_columns,
+ .dict_filter_columns = dict_filter_columns,
+ .dict_filter_unsupported_columns = dict_filter_unsupported_columns,
+ .dict_filter_read_failures = dict_filter_read_failures,
+ .rows_filtered_by_dict_filter = rows_filtered_by_dict_filter,
.column_reader_profile = column_reader_profile(),
};
}
diff --git a/be/src/format_v2/parquet/parquet_profile.h
b/be/src/format_v2/parquet/parquet_profile.h
index 91c8ad89ddc..27f9818f4a0 100644
--- a/be/src/format_v2/parquet/parquet_profile.h
+++ b/be/src/format_v2/parquet/parquet_profile.h
@@ -52,7 +52,18 @@ struct ParquetScanProfile {
RuntimeProfile::Counter* range_gap_skipped_rows = nullptr; // rows skipped
by range gaps
RuntimeProfile::Counter* column_read_time = nullptr; // column read
time (ns)
RuntimeProfile::Counter* predicate_filter_time = nullptr; // predicate
filter time (ns)
- ParquetColumnReaderProfile column_reader_profile; // nested
column read statistics
+ RuntimeProfile::Counter* dict_filter_rewrite_time = nullptr; // dictionary
rewrite time (ns)
+ RuntimeProfile::Counter* dict_filter_expr_rewrite_time =
+ nullptr; // expression/residual rewrite time (ns)
+ RuntimeProfile::Counter* dict_filter_read_dict_time = nullptr; //
dictionary page read time (ns)
+ RuntimeProfile::Counter* dict_filter_build_time =
+ nullptr; // dictionary entry bitmap build time (ns)
+ RuntimeProfile::Counter* dict_filter_candidate_columns = nullptr; //
candidate columns
+ RuntimeProfile::Counter* dict_filter_columns = nullptr; //
optimized columns
+ RuntimeProfile::Counter* dict_filter_unsupported_columns = nullptr; //
unsupported columns
+ RuntimeProfile::Counter* dict_filter_read_failures = nullptr; //
dictionary read failures
+ RuntimeProfile::Counter* rows_filtered_by_dict_filter = nullptr; //
rows filtered by dict
+ ParquetColumnReaderProfile column_reader_profile; // nested column read
statistics
};
// ============================================================================
@@ -137,6 +148,14 @@ struct ParquetProfile {
RuntimeProfile::Counter* predicate_filter_time = nullptr;
RuntimeProfile::Counter* dict_filter_rewrite_time = nullptr;
+ RuntimeProfile::Counter* dict_filter_expr_rewrite_time = nullptr;
+ RuntimeProfile::Counter* dict_filter_read_dict_time = nullptr;
+ RuntimeProfile::Counter* dict_filter_build_time = nullptr;
+ RuntimeProfile::Counter* dict_filter_candidate_columns = nullptr;
+ RuntimeProfile::Counter* dict_filter_columns = nullptr;
+ RuntimeProfile::Counter* dict_filter_unsupported_columns = nullptr;
+ RuntimeProfile::Counter* dict_filter_read_failures = nullptr;
+ RuntimeProfile::Counter* rows_filtered_by_dict_filter = nullptr;
RuntimeProfile::Counter* convert_time = nullptr;
RuntimeProfile::Counter* bloom_filter_read_time = nullptr;
};
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp
b/be/src/format_v2/parquet/parquet_scan.cpp
index 86de8be9f92..99bfcc8e0ab 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -15,10 +15,13 @@
#include "format_v2/parquet/parquet_scan.h"
+#include <parquet/encoding.h>
+
#include <algorithm>
#include <limits>
#include <memory>
#include <ranges>
+#include <set>
#include <unordered_set>
#include <utility>
@@ -27,6 +30,7 @@
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column_vector.h"
+#include "exprs/vcompound_pred.h"
#include "exprs/vexpr_context.h"
#include "format_v2/parquet/parquet_column_schema.h"
#include "format_v2/parquet/parquet_file_context.h"
@@ -42,6 +46,70 @@ int64_t column_start_offset(const
::parquet::ColumnChunkMetaData& column_metadat
: cast_set<int64_t>(column_metadata.data_page_offset());
}
+bool is_dictionary_data_encoding(::parquet::Encoding::type encoding) {
+ return encoding == ::parquet::Encoding::PLAIN_DICTIONARY ||
+ encoding == ::parquet::Encoding::RLE_DICTIONARY;
+}
+
+bool is_level_encoding(::parquet::Encoding::type encoding) {
+ return encoding == ::parquet::Encoding::RLE || encoding ==
::parquet::Encoding::BIT_PACKED;
+}
+
+bool is_data_page_type(::parquet::PageType::type page_type) {
+ return page_type == ::parquet::PageType::DATA_PAGE ||
+ page_type == ::parquet::PageType::DATA_PAGE_V2;
+}
+
+bool is_fully_dictionary_encoded_chunk(const ::parquet::ColumnChunkMetaData&
column_metadata) {
+ if (!column_metadata.has_dictionary_page()) {
+ return false;
+ }
+
+ const auto& encoding_stats = column_metadata.encoding_stats();
+ if (!encoding_stats.empty()) {
+ bool has_dictionary_data_page = false;
+ for (const auto& encoding_stat : encoding_stats) {
+ if (!is_data_page_type(encoding_stat.page_type) ||
encoding_stat.count <= 0) {
+ continue;
+ }
+ if (!is_dictionary_data_encoding(encoding_stat.encoding)) {
+ return false;
+ }
+ has_dictionary_data_page = true;
+ }
+ return has_dictionary_data_page;
+ }
+
+ bool has_dictionary_encoding = false;
+ for (const auto encoding : column_metadata.encodings()) {
+ if (is_dictionary_data_encoding(encoding)) {
+ has_dictionary_encoding = true;
+ continue;
+ }
+ if (!is_level_encoding(encoding)) {
+ return false;
+ }
+ }
+ return has_dictionary_encoding;
+}
+
+bool supports_row_level_dictionary_filter(const ParquetColumnSchema&
column_schema,
+ const
::parquet::ColumnChunkMetaData& column_metadata) {
+ if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE ||
+ column_schema.descriptor == nullptr || column_schema.type == nullptr ||
+ column_schema.max_repetition_level > 0) {
+ return false;
+ }
+ if (!column_schema.type_descriptor.is_string_like ||
+ column_metadata.type() != ::parquet::Type::BYTE_ARRAY) {
+ return false;
+ }
+ // Row-level dictionary filtering consumes dictionary ids from DATA_PAGE
payloads. It is exact
+ // only when every data page is dictionary encoded. Mixed dictionary/plain
chunks are left on
+ // the normal decoded-value path, matching the safety rule used by
StarRocks and Doris v1.
+ return is_fully_dictionary_encoded_chunk(column_metadata);
+}
+
void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
std::unordered_set<int>* leaf_column_ids) {
DORIS_CHECK(leaf_column_ids != nullptr);
@@ -270,6 +338,15 @@ Status plan_parquet_row_groups(const
::parquet::FileMetaData& metadata,
namespace {
+using DictionaryResidualConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
+using DictionaryResidualConjuncts = std::vector<DictionaryResidualConjunct>;
+
+void update_counter_if_not_null(RuntimeProfile::Counter* counter, int64_t
value) {
+ if (counter != nullptr) {
+ COUNTER_UPDATE(counter, value);
+ }
+}
+
uint16_t apply_filter_to_selection(const IColumn::Filter& filter,
SelectionVector* selection,
uint16_t selected_rows) {
uint16_t new_selected_rows = 0;
@@ -282,6 +359,91 @@ uint16_t apply_filter_to_selection(const IColumn::Filter&
filter, SelectionVecto
return new_selected_rows;
}
+Status execute_compact_filter_conjuncts(const VExprContextSPtrs& conjuncts,
size_t rows,
+ Block* file_block, IColumn::Filter*
compact_filter,
+ bool* can_filter_all) {
+ DORIS_CHECK(compact_filter != nullptr);
+ DORIS_CHECK(can_filter_all != nullptr);
+ compact_filter->resize_fill(rows, 1);
+ *can_filter_all = false;
+ for (const auto& conjunct : conjuncts) {
+ DORIS_CHECK(conjunct != nullptr);
+ IColumn::Filter filter(rows, 1);
+ bool conjunct_can_filter_all = false;
+ RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(),
rows, false,
+ &conjunct_can_filter_all));
+ if (conjunct_can_filter_all) {
+ std::ranges::fill(*compact_filter, 0);
+ *can_filter_all = true;
+ break;
+ }
+ for (size_t row = 0; row < rows; ++row) {
+ (*compact_filter)[row] &= filter[row];
+ }
+ }
+ return Status::OK();
+}
+
+Status execute_compact_dictionary_residual_conjuncts(const
DictionaryResidualConjuncts& conjuncts,
+ size_t rows, Block*
file_block,
+ IColumn::Filter*
compact_filter,
+ bool* can_filter_all) {
+ DORIS_CHECK(compact_filter != nullptr);
+ DORIS_CHECK(can_filter_all != nullptr);
+ compact_filter->resize_fill(rows, 1);
+ *can_filter_all = false;
+ for (const auto& [owner_context, residual_expr] : conjuncts) {
+ DORIS_CHECK(owner_context != nullptr);
+ DORIS_CHECK(residual_expr != nullptr);
+ IColumn::Filter filter(rows, 1);
+ bool conjunct_can_filter_all = false;
+ RETURN_IF_ERROR(residual_expr->execute_filter(owner_context.get(),
file_block,
+ filter.data(), rows,
false,
+
&conjunct_can_filter_all));
+ if (conjunct_can_filter_all) {
+ std::ranges::fill(*compact_filter, 0);
+ *can_filter_all = true;
+ break;
+ }
+ for (size_t row = 0; row < rows; ++row) {
+ (*compact_filter)[row] &= filter[row];
+ }
+ }
+ return Status::OK();
+}
+
+Status execute_compact_delete_conjuncts(const VExprContextSPtrs&
delete_conjuncts, size_t rows,
+ Block* file_block, IColumn::Filter*
compact_filter,
+ bool* can_filter_all) {
+ DORIS_CHECK(compact_filter != nullptr);
+ DORIS_CHECK(can_filter_all != nullptr);
+ compact_filter->resize_fill(rows, 1);
+ *can_filter_all = false;
+ for (const auto& delete_conjunct : delete_conjuncts) {
+ DORIS_CHECK(delete_conjunct != nullptr);
+ int result_column_id = -1;
+
RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(),
file_block,
+ &result_column_id));
+ DORIS_CHECK(result_column_id >= 0 &&
+ result_column_id <
static_cast<int>(file_block->columns()));
+ const auto& delete_filter = assert_cast<const ColumnUInt8&>(
+
*file_block->get_by_position(result_column_id).column)
+ .get_data();
+ DORIS_CHECK(delete_filter.size() == rows);
+ bool has_kept_row = false;
+ for (size_t row = 0; row < rows; ++row) {
+ (*compact_filter)[row] &= !delete_filter[row];
+ has_kept_row |= (*compact_filter)[row] != 0;
+ }
+ file_block->erase(result_column_id);
+ if (!has_kept_row) {
+ *can_filter_all = true;
+ break;
+ }
+ }
+ return Status::OK();
+}
+
Status execute_filter_conjuncts(const format::FileScanRequest& request,
int64_t batch_rows,
Block* file_block, SelectionVector* selection,
uint16_t* selected_rows) {
@@ -334,6 +496,20 @@ Status execute_delete_conjuncts(const
format::FileScanRequest& request, int64_t
} // namespace
+uint16_t apply_compact_filter_to_selection(const IColumn::Filter& filter,
+ SelectionVector* selection,
uint16_t selected_rows) {
+ DORIS_CHECK(selection != nullptr);
+ DORIS_CHECK(filter.size() == selected_rows);
+ uint16_t new_selected_rows = 0;
+ for (uint16_t selection_idx = 0; selection_idx < selected_rows;
++selection_idx) {
+ if (filter[selection_idx] != 0) {
+ selection->set_index(new_selected_rows++,
static_cast<SelectionVector::Index>(
+
selection->get_index(selection_idx)));
+ }
+ }
+ return new_selected_rows;
+}
+
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t
selected_rows,
int64_t batch_rows) {
IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
@@ -462,6 +638,8 @@ void ParquetScanScheduler::reset_current_row_group() {
_current_row_group.reset();
_current_predicate_columns.clear();
_current_non_predicate_columns.clear();
+ _current_dictionary_filters.clear();
+ _current_dictionary_residual_conjuncts.clear();
_current_row_group_rows = 0;
_current_row_group_id = -1;
_current_row_group_rows_read = 0;
@@ -484,8 +662,12 @@ Status ParquetScanScheduler::open_next_row_group(
}
const RowGroupReadPlan& row_group_plan =
_row_group_plans[_next_row_group_plan_idx++];
const int row_group_idx = row_group_plan.row_group_id;
- _current_merge_range_active =
- prepare_current_row_group_reader(file_context, file_schema,
request, row_group_idx);
+ // Row-level dictionary filters read dictionary pages before Arrow
RecordReaders are created.
+ // Keep that probe on the base reader: MergeRangeFileReader expects each
registered range to be
+ // consumed as one forward pass, while the later RecordReader opens the
same column chunk again
+ // for the data-page stream.
+ file_context.reset_random_access_ranges();
+ _current_merge_range_active = false;
try {
_current_row_group = file_context.file_reader->RowGroup(row_group_idx);
} catch (const ::parquet::ParquetException& e) {
@@ -510,6 +692,11 @@ Status ParquetScanScheduler::open_next_row_group(
_current_range_rows_read = 0;
_current_predicate_columns.clear();
_current_non_predicate_columns.clear();
+ _current_dictionary_filters.clear();
+ RETURN_IF_ERROR(prepare_current_dictionary_filters(file_context,
file_schema, request,
+ row_group_idx,
*row_group_metadata));
+ _current_merge_range_active =
+ prepare_current_row_group_reader(file_context, file_schema,
request, row_group_idx);
ParquetColumnReaderFactory column_reader_factory(
_current_row_group, file_context.schema->num_columns(),
&row_group_plan.page_skip_plans,
@@ -535,7 +722,9 @@ Status ParquetScanScheduler::open_next_row_group(
const auto& column_schema = file_schema[local_id];
DORIS_CHECK(column_schema != nullptr);
std::unique_ptr<ParquetColumnReader> column_reader;
- RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col,
&column_reader));
+ RETURN_IF_ERROR(
+ column_reader_factory.create(*column_schema, &col,
&column_reader,
+
_current_dictionary_filters.contains(local_id)));
_current_predicate_columns[local_id] = std::move(column_reader);
}
// Start warming filter-column chunks as soon as their row group is
selected. Parquet v2 still
@@ -597,42 +786,517 @@ Status
ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
return Status::OK();
}
+namespace {
+
+struct PredicateConjunctSchedule {
+ std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
+ VExprContextSPtrs remaining_conjuncts;
+};
+
+PredicateConjunctSchedule build_predicate_conjunct_schedule(
+ const format::FileScanRequest& request) {
+ std::unordered_set<size_t> predicate_block_positions;
+ predicate_block_positions.reserve(request.predicate_columns.size());
+ for (const auto& col : request.predicate_columns) {
+ const auto position_it = request.local_positions.find(col.column_id());
+ DORIS_CHECK(position_it != request.local_positions.end());
+ predicate_block_positions.insert(position_it->second.value());
+ }
+
+ PredicateConjunctSchedule schedule;
+ for (const auto& conjunct : request.conjuncts) {
+ DORIS_CHECK(conjunct != nullptr);
+ DORIS_CHECK(conjunct->root() != nullptr);
+ if (!conjunct->root()->is_safe_to_execute_on_selected_rows()) {
+ // Round-by-round filtering can compact later predicate columns
before evaluating
+ // remaining expressions. Stateful functions such as random(1) and
error-preserving
+ // functions such as assert_true() must see the same full batch
they saw before this
+ // optimization, so any unsafe conjunct disables the per-column
schedule for the batch.
+ schedule.remaining_conjuncts = request.conjuncts;
+ schedule.single_column_conjuncts.clear();
+ return schedule;
+ }
+ std::set<int> referenced_positions;
+ conjunct->root()->collect_slot_column_ids(referenced_positions);
+ if (referenced_positions.size() != 1) {
+ schedule.remaining_conjuncts.push_back(conjunct);
+ continue;
+ }
+ const auto block_position =
static_cast<size_t>(*referenced_positions.begin());
+ if (!predicate_block_positions.contains(block_position)) {
+ schedule.remaining_conjuncts.push_back(conjunct);
+ continue;
+ }
+ schedule.single_column_conjuncts[block_position].push_back(conjunct);
+ }
+ return schedule;
+}
+
+bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
+ if (conjuncts.empty()) {
+ return false;
+ }
+ return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
+ return conjunct != nullptr && conjunct->root() != nullptr &&
+ conjunct->root()->can_evaluate_dictionary_filter();
+ });
+}
+
+bool can_evaluate_dictionary_exactly(const VExprSPtr& expr) {
+ DORIS_CHECK(expr != nullptr);
+ const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
+ if (compound_pred == nullptr) {
+ return expr->can_evaluate_dictionary_filter();
+ }
+ if (compound_pred->op() != TExprOpcode::COMPOUND_AND &&
+ compound_pred->op() != TExprOpcode::COMPOUND_OR) {
+ return false;
+ }
+ return !expr->children().empty() &&
+ std::ranges::all_of(expr->children(), [](const auto& child) {
+ return can_evaluate_dictionary_exactly(child);
+ });
+}
+
+void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context,
const VExprSPtr& expr,
+ DictionaryResidualConjuncts*
residual_conjuncts) {
+ DORIS_CHECK(owner_context != nullptr);
+ DORIS_CHECK(expr != nullptr);
+ DORIS_CHECK(residual_conjuncts != nullptr);
+
+ if (can_evaluate_dictionary_exactly(expr)) {
+ return;
+ }
+
+ // VCompoundPred dictionary evaluation is a conservative prefilter for AND
when only some
+ // children are dictionary-aware. Split AND so exact dictionary children
are not executed again
+ // on materialized rows. Do not split a non-exact OR: its branches cannot
be evaluated
+ // independently after a dictionary prefilter without changing the
original boolean semantics.
+ const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
+ if (compound_pred != nullptr && compound_pred->op() ==
TExprOpcode::COMPOUND_AND) {
+ for (const auto& child : expr->children()) {
+ collect_dictionary_residual_exprs(owner_context, child,
residual_conjuncts);
+ }
+ return;
+ }
+
+ residual_conjuncts->emplace_back(owner_context, expr);
+}
+
+DictionaryResidualConjuncts build_dictionary_residual_conjuncts(
+ const VExprContextSPtrs& conjuncts) {
+ DictionaryResidualConjuncts residual_conjuncts;
+ for (const auto& conjunct : conjuncts) {
+ DORIS_CHECK(conjunct != nullptr);
+ collect_dictionary_residual_exprs(conjunct, conjunct->root(),
&residual_conjuncts);
+ }
+ return residual_conjuncts;
+}
+
+uint16_t count_selected_rows(const IColumn::Filter& filter) {
+ uint16_t selected_rows = 0;
+ for (const auto value : filter) {
+ selected_rows += value != 0;
+ }
+ return selected_rows;
+}
+
+Status filter_read_predicate_columns(Block* file_block, const
std::vector<uint32_t>& positions,
+ const IColumn::Filter& compact_filter) {
+ if (positions.empty()) {
+ return Status::OK();
+ }
+ RETURN_IF_CATCH_EXCEPTION(Block::filter_block_internal(file_block,
positions, compact_filter));
+ return Status::OK();
+}
+
+IColumn::Filter build_dictionary_entry_filter(size_t block_position,
+ const ParquetColumnSchema&
column_schema,
+ const VExprContextSPtrs&
conjuncts,
+ const ParquetDictionaryWords&
dict_words) {
+ auto fields = dictionary_fields_from_words(dict_words);
+ IColumn::Filter dictionary_filter(fields.size(), 1);
+ DictionaryEvalContext ctx;
+ auto& slot = ctx.slots
+ .emplace(static_cast<int>(block_position),
+ DictionaryEvalContext::SlotDictionary {
+ .data_type = column_schema.type,
.values = {}})
+ .first->second;
+ slot.values.reserve(1);
+
+ for (size_t dict_idx = 0; dict_idx < fields.size(); ++dict_idx) {
+ slot.values.clear();
+ slot.values.push_back(fields[dict_idx]);
+ dictionary_filter[dict_idx] =
VExprContext::evaluate_dictionary_filter(conjuncts, ctx) ==
+
ZoneMapFilterResult::kNoMatch
+ ? 0
+ : 1;
+ }
+ return dictionary_filter;
+}
+
+} // namespace
+
+Status ParquetScanScheduler::prepare_current_dictionary_filters(
+ ParquetFileContext& file_context,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+ const format::FileScanRequest& request, int row_group_idx,
+ const ::parquet::RowGroupMetaData& row_group_metadata) {
+ _current_dictionary_filters.clear();
+ _current_dictionary_residual_conjuncts.clear();
+ if (request.conjuncts.empty()) {
+ return Status::OK();
+ }
+ PredicateConjunctSchedule schedule;
+ {
+ SCOPED_TIMER(_scan_profile.dict_filter_expr_rewrite_time);
+ schedule = build_predicate_conjunct_schedule(request);
+ }
+ if (schedule.single_column_conjuncts.empty()) {
+ return Status::OK();
+ }
+
+ SCOPED_TIMER(_scan_profile.dict_filter_rewrite_time);
+ for (const auto& col : request.predicate_columns) {
+ const auto local_id = col.local_id();
+ if (local_id < 0 || local_id >=
static_cast<int32_t>(file_schema.size())) {
+ continue;
+ }
+ const auto position_it = request.local_positions.find(col.column_id());
+ DORIS_CHECK(position_it != request.local_positions.end());
+ const auto block_position =
static_cast<size_t>(position_it->second.value());
+ const auto conjunct_it =
schedule.single_column_conjuncts.find(block_position);
+ if (conjunct_it == schedule.single_column_conjuncts.end() ||
+ !can_evaluate_all_with_dictionary(conjunct_it->second)) {
+ continue;
+ }
+
update_counter_if_not_null(_scan_profile.dict_filter_candidate_columns, 1);
+
+ // This optimization is deliberately limited to single-column
predicates with a dictionary
+ // evaluable part. Mixed AND predicates are split so
dictionary-covered children run as a
+ // dict-id prefilter and residual children keep the normal row-level
expression path.
+ const auto& column_schema = file_schema[local_id];
+ DORIS_CHECK(column_schema != nullptr);
+ if (column_schema->leaf_column_id < 0 ||
+ column_schema->leaf_column_id >= row_group_metadata.num_columns())
{
+
update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
+ continue;
+ }
+ auto column_chunk =
row_group_metadata.ColumnChunk(column_schema->leaf_column_id);
+ if (column_chunk == nullptr ||
+ !supports_row_level_dictionary_filter(*column_schema,
*column_chunk)) {
+
update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
+ continue;
+ }
+
+ ParquetDictionaryWords dict_words;
+ {
+ SCOPED_TIMER(_scan_profile.dict_filter_read_dict_time);
+ if (!read_dictionary_words(file_context.file_reader.get(),
row_group_idx,
+ column_schema->leaf_column_id,
*column_schema,
+ &dict_words)) {
+
update_counter_if_not_null(_scan_profile.dict_filter_read_failures, 1);
+ continue;
+ }
+ }
+
+ // Build a safe dictionary prefilter from the dictionary-filter
interface instead of
+ // executing the row expression on a temporary dictionary block. For
compound AND,
+ // VCompoundPred intentionally evaluates only dictionary-capable
children, so residual
+ // predicates still run later on surviving rows.
+ IColumn::Filter dictionary_filter;
+ DictionaryResidualConjuncts residual_conjuncts;
+ {
+ SCOPED_TIMER(_scan_profile.dict_filter_build_time);
+ dictionary_filter = build_dictionary_entry_filter(block_position,
*column_schema,
+
conjunct_it->second, dict_words);
+ residual_conjuncts =
build_dictionary_residual_conjuncts(conjunct_it->second);
+ }
+
+ // The bitmap is keyed by Parquet dictionary id. Later data-page reads
evaluate the
+ // predicate with an integer lookup and only materialize STRING values
for surviving rows.
+ _current_dictionary_filters.emplace(local_id,
std::move(dictionary_filter));
+ _current_dictionary_residual_conjuncts.emplace(local_id,
std::move(residual_conjuncts));
+ update_counter_if_not_null(_scan_profile.dict_filter_columns, 1);
+ }
+ return Status::OK();
+}
+
Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
const
format::FileScanRequest& request,
Block* file_block,
SelectionVector* selection,
uint16_t* selected_rows,
- int64_t*
conjunct_filtered_rows) {
+ int64_t*
conjunct_filtered_rows,
+ bool*
predicate_columns_filtered) {
+ DORIS_CHECK(predicate_columns_filtered != nullptr);
+ *predicate_columns_filtered = false;
if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
selection->resize(static_cast<size_t>(batch_rows));
}
- for (const auto& [fid, column_reader] : _current_predicate_columns) {
- auto position_it =
request.local_positions.find(format::LocalColumnId(fid));
- DORIS_CHECK(position_it != request.local_positions.end());
- const auto block_position = position_it->second.value();
+ const auto schedule = build_predicate_conjunct_schedule(request);
+ const bool can_read_predicate_columns_round_by_round =
+ !schedule.single_column_conjuncts.empty();
+ std::vector<uint32_t> read_column_positions;
+ read_column_positions.reserve(request.predicate_columns.size());
+
+ auto read_predicate_column = [&](ParquetColumnReader* column_reader,
size_t block_position,
+ ColumnId local_id, bool*
used_dictionary_filter) -> Status {
+ DORIS_CHECK(used_dictionary_filter != nullptr);
+ *used_dictionary_filter = false;
DCHECK(remove_nullable(column_reader->type())
->equals(*remove_nullable(file_block->get_by_position(block_position).type)))
<< column_reader->type()->get_name() << " "
<<
file_block->get_by_position(block_position).type->get_name() << " "
<< column_reader->name() << " " <<
file_block->get_by_position(block_position).name;
auto column =
file_block->get_by_position(block_position).column->assert_mutable();
- int64_t column_rows = 0;
- {
- SCOPED_TIMER(_scan_profile.column_read_time);
- RETURN_IF_ERROR(column_reader->read(batch_rows, column,
&column_rows));
+ SCOPED_TIMER(_scan_profile.column_read_time);
+ const auto dictionary_filter_it =
_current_dictionary_filters.find(local_id);
+ if (dictionary_filter_it != _current_dictionary_filters.end()) {
+ const uint16_t selected_rows_before = *selected_rows;
+ IColumn::Filter compact_filter;
+ bool used_filter = false;
+ RETURN_IF_ERROR(column_reader->select_with_dictionary_filter(
+ *selection, *selected_rows, batch_rows,
dictionary_filter_it->second, column,
+ &compact_filter, &used_filter));
+ if (used_filter) {
+ DORIS_CHECK(compact_filter.size() == selected_rows_before);
+ const uint16_t new_selected_rows =
count_selected_rows(compact_filter);
+ const auto filtered_rows =
static_cast<int64_t>(selected_rows_before) -
+
static_cast<int64_t>(new_selected_rows);
+ if (conjunct_filtered_rows != nullptr) {
+ *conjunct_filtered_rows += filtered_rows;
+ }
+
update_counter_if_not_null(_scan_profile.rows_filtered_by_dict_filter,
+ filtered_rows);
+ if (new_selected_rows != selected_rows_before) {
+ // The dictionary reader has already appended only
surviving values for the
+ // current column. Apply the compact row filter only to
columns read before this
+ // one, then update the shared selection for later
predicate/lazy columns.
+ RETURN_IF_ERROR(filter_read_predicate_columns(file_block,
read_column_positions,
+
compact_filter));
+ *selected_rows =
apply_compact_filter_to_selection(compact_filter, selection,
+
selected_rows_before);
+ *predicate_columns_filtered = true;
+ }
+ file_block->replace_by_position(block_position,
std::move(column));
+
read_column_positions.push_back(cast_set<uint32_t>(block_position));
+ *used_dictionary_filter = true;
+ return Status::OK();
+ }
}
- if (column_rows != batch_rows) {
- return Status::Corruption("Parquet filter column {} returned {}
rows, expected {} rows",
- column_reader->name(), column_rows,
batch_rows);
+
+ if (*selected_rows == batch_rows) {
+ int64_t column_rows = 0;
+ RETURN_IF_ERROR(column_reader->read(batch_rows, column,
&column_rows));
+ if (column_rows != batch_rows) {
+ return Status::Corruption(
+ "Parquet filter column {} returned {} rows, expected
{} rows",
+ column_reader->name(), column_rows, batch_rows);
+ }
+ } else {
+ [[maybe_unused]] auto old_size = column->size();
+ RETURN_IF_ERROR(column_reader->select(*selection, *selected_rows,
batch_rows, column));
+ if (column->size() != old_size + *selected_rows) {
+ return Status::Corruption(
+ "Parquet selected filter column {} returned {} rows,
expected {} rows",
+ column_reader->name(), column->size(), old_size +
*selected_rows);
+ }
+ *predicate_columns_filtered = true;
}
file_block->replace_by_position(block_position, std::move(column));
- }
- if (_scan_profile.predicate_filter_time == nullptr) {
+ read_column_positions.push_back(cast_set<uint32_t>(block_position));
+ return Status::OK();
+ };
+
+ auto execute_scheduled_conjuncts = [&](const VExprContextSPtrs& conjuncts)
-> Status {
+ if (conjuncts.empty() || *selected_rows == 0) {
+ return Status::OK();
+ }
+ const uint16_t selected_rows_before = *selected_rows;
+ IColumn::Filter compact_filter;
+ bool can_filter_all = false;
+ RETURN_IF_ERROR(execute_compact_filter_conjuncts(
+ conjuncts, selected_rows_before, file_block, &compact_filter,
&can_filter_all));
+ if (can_filter_all) {
+ compact_filter.resize_fill(selected_rows_before, 0);
+ }
+ const uint16_t new_selected_rows = can_filter_all ? 0 :
count_selected_rows(compact_filter);
+ if (conjunct_filtered_rows != nullptr) {
+ *conjunct_filtered_rows +=
static_cast<int64_t>(selected_rows_before) -
+ static_cast<int64_t>(new_selected_rows);
+ }
+ if (new_selected_rows != selected_rows_before) {
+ // All columns read so far are already compacted to the current
selection. Apply the
+ // compact filter to those columns and the selection vector
together, so later predicate
+ // columns can read only rows that survived previous predicate
rounds.
+ RETURN_IF_ERROR(filter_read_predicate_columns(file_block,
read_column_positions,
+ compact_filter));
+ *selected_rows = can_filter_all
+ ? 0
+ :
apply_compact_filter_to_selection(compact_filter, selection,
+
selected_rows_before);
+ *predicate_columns_filtered = true;
+ }
+ return Status::OK();
+ };
+
+ auto execute_scheduled_dictionary_residual_conjuncts =
+ [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+ if (conjuncts.empty() || *selected_rows == 0) {
+ return Status::OK();
+ }
+ const uint16_t selected_rows_before = *selected_rows;
+ IColumn::Filter compact_filter;
+ bool can_filter_all = false;
+ RETURN_IF_ERROR(execute_compact_dictionary_residual_conjuncts(
+ conjuncts, selected_rows_before, file_block, &compact_filter,
&can_filter_all));
+ if (can_filter_all) {
+ compact_filter.resize_fill(selected_rows_before, 0);
+ }
+ const uint16_t new_selected_rows = can_filter_all ? 0 :
count_selected_rows(compact_filter);
+ if (conjunct_filtered_rows != nullptr) {
+ *conjunct_filtered_rows +=
static_cast<int64_t>(selected_rows_before) -
+ static_cast<int64_t>(new_selected_rows);
+ }
+ if (new_selected_rows != selected_rows_before) {
+ // Dictionary-covered children have already reduced the compact
block. Apply only the
+ // residual child filters here, then keep the same
compacted-column invariant as the
+ // normal conjunct path for later predicate rounds.
+ RETURN_IF_ERROR(filter_read_predicate_columns(file_block,
read_column_positions,
+ compact_filter));
+ *selected_rows = can_filter_all
+ ? 0
+ :
apply_compact_filter_to_selection(compact_filter, selection,
+
selected_rows_before);
+ *predicate_columns_filtered = true;
+ }
+ return Status::OK();
+ };
+
+ auto execute_scheduled_conjuncts_with_profile =
+ [&](const VExprContextSPtrs& conjuncts) -> Status {
+ if (_scan_profile.predicate_filter_time == nullptr) {
+ return execute_scheduled_conjuncts(conjuncts);
+ }
+ SCOPED_TIMER(_scan_profile.predicate_filter_time);
+ return execute_scheduled_conjuncts(conjuncts);
+ };
+
+ auto execute_scheduled_dictionary_residual_conjuncts_with_profile =
+ [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+ if (_scan_profile.predicate_filter_time == nullptr) {
+ return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+ }
+ SCOPED_TIMER(_scan_profile.predicate_filter_time);
+ return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+ };
+
+ auto execute_scheduled_delete_conjuncts = [&]() -> Status {
+ if (request.delete_conjuncts.empty() || *selected_rows == 0) {
+ return Status::OK();
+ }
+ const uint16_t selected_rows_before = *selected_rows;
+ IColumn::Filter compact_filter;
+ bool can_filter_all = false;
+
RETURN_IF_ERROR(execute_compact_delete_conjuncts(request.delete_conjuncts,
+ selected_rows_before,
file_block,
+ &compact_filter,
&can_filter_all));
+ if (can_filter_all) {
+ compact_filter.resize_fill(selected_rows_before, 0);
+ }
+ if (can_filter_all || count_selected_rows(compact_filter) !=
selected_rows_before) {
+ RETURN_IF_ERROR(filter_read_predicate_columns(file_block,
read_column_positions,
+ compact_filter));
+ *selected_rows = can_filter_all
+ ? 0
+ :
apply_compact_filter_to_selection(compact_filter, selection,
+
selected_rows_before);
+ *predicate_columns_filtered = true;
+ }
+ return Status::OK();
+ };
+
+ auto read_all_predicate_columns = [&]() -> Status {
+ for (const auto& [fid, column_reader] : _current_predicate_columns) {
+ auto position_it =
request.local_positions.find(format::LocalColumnId(fid));
+ DORIS_CHECK(position_it != request.local_positions.end());
+ bool used_dictionary_filter = false;
+ RETURN_IF_ERROR(read_predicate_column(column_reader.get(),
position_it->second.value(),
+ fid,
&used_dictionary_filter));
+ }
+ return Status::OK();
+ };
+
+ if (!can_read_predicate_columns_round_by_round) {
+ RETURN_IF_ERROR(read_all_predicate_columns());
+ if (_scan_profile.predicate_filter_time == nullptr) {
+ return execute_batch_filters(request, batch_rows, file_block,
selection, selected_rows,
+ conjunct_filtered_rows);
+ }
+ SCOPED_TIMER(_scan_profile.predicate_filter_time);
return execute_batch_filters(request, batch_rows, file_block,
selection, selected_rows,
conjunct_filtered_rows);
}
+
+ auto read_round_by_round = [&]() -> Status {
+ // Single-column conjuncts can be evaluated immediately after their
column is read. Once
+ // selection shrinks, later predicate columns use
ParquetColumnReader::select() so the
+ // reader skips rows already rejected by earlier predicates instead of
materializing them.
+ for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
+ const auto& col = request.predicate_columns[idx];
+ const auto fid = col.local_id();
+ auto reader_it = _current_predicate_columns.find(fid);
+ DORIS_CHECK(reader_it != _current_predicate_columns.end());
+ auto position_it = request.local_positions.find(col.column_id());
+ DORIS_CHECK(position_it != request.local_positions.end());
+ const auto block_position = position_it->second.value();
+ bool used_dictionary_filter = false;
+ RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(),
block_position, fid,
+ &used_dictionary_filter));
+ if (*selected_rows == 0) {
+ for (size_t remaining_idx = idx + 1;
+ remaining_idx < request.predicate_columns.size();
++remaining_idx) {
+ const auto remaining_fid =
request.predicate_columns[remaining_idx].local_id();
+ auto remaining_reader_it =
_current_predicate_columns.find(remaining_fid);
+ DORIS_CHECK(remaining_reader_it !=
_current_predicate_columns.end());
+
RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
+ }
+ return Status::OK();
+ }
+ const auto conjunct_it =
schedule.single_column_conjuncts.find(block_position);
+ if (conjunct_it == schedule.single_column_conjuncts.end()) {
+ continue;
+ }
+ if (used_dictionary_filter) {
+ const auto residual_it =
_current_dictionary_residual_conjuncts.find(fid);
+ DORIS_CHECK(residual_it !=
_current_dictionary_residual_conjuncts.end());
+
RETURN_IF_ERROR(execute_scheduled_dictionary_residual_conjuncts_with_profile(
+ residual_it->second));
+ } else {
+
RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));
+ }
+ if (*selected_rows != 0) {
+ continue;
+ }
+ for (size_t remaining_idx = idx + 1; remaining_idx <
request.predicate_columns.size();
+ ++remaining_idx) {
+ const auto remaining_fid =
request.predicate_columns[remaining_idx].local_id();
+ auto remaining_reader_it =
_current_predicate_columns.find(remaining_fid);
+ DORIS_CHECK(remaining_reader_it !=
_current_predicate_columns.end());
+ RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
+ }
+ return Status::OK();
+ }
+ return Status::OK();
+ };
+
+ RETURN_IF_ERROR(read_round_by_round());
+
RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(schedule.remaining_conjuncts));
+ if (_scan_profile.predicate_filter_time == nullptr) {
+ return execute_scheduled_delete_conjuncts();
+ }
SCOPED_TIMER(_scan_profile.predicate_filter_time);
- return execute_batch_filters(request, batch_rows, file_block, selection,
selected_rows,
- conjunct_filtered_rows);
+ return execute_scheduled_delete_conjuncts();
}
bool ParquetScanScheduler::prepare_current_row_group_reader(
@@ -692,8 +1356,9 @@ Status ParquetScanScheduler::read_current_row_group_batch(
DORIS_CHECK(batch_rows <= std::numeric_limits<uint16_t>::max());
uint16_t selected_rows = static_cast<uint16_t>(batch_rows);
int64_t conjunct_filtered_rows = 0;
+ bool predicate_columns_filtered = false;
RETURN_IF_ERROR(read_filter_columns(batch_rows, request, file_block,
&selection, &selected_rows,
- &conjunct_filtered_rows));
+ &conjunct_filtered_rows,
&predicate_columns_filtered));
_predicate_filtered_rows += conjunct_filtered_rows;
mark_condition_cache_granules(selection, selected_rows,
batch_first_file_row);
@@ -711,7 +1376,7 @@ Status ParquetScanScheduler::read_current_row_group_batch(
if (selected_rows == 0 && _scan_profile.empty_selection_batches !=
nullptr) {
COUNTER_UPDATE(_scan_profile.empty_selection_batches, 1);
}
- if (need_filter_output) {
+ if (need_filter_output && !predicate_columns_filtered) {
IColumn::Filter output_filter = selection_to_filter(selection,
selected_rows, batch_rows);
for (const auto& col : request.predicate_columns) {
auto position_it = request.local_positions.find(col.column_id());
diff --git a/be/src/format_v2/parquet/parquet_scan.h
b/be/src/format_v2/parquet/parquet_scan.h
index 1fb313d4f75..3fa7586fb1b 100644
--- a/be/src/format_v2/parquet/parquet_scan.h
+++ b/be/src/format_v2/parquet/parquet_scan.h
@@ -20,6 +20,7 @@
#include <map>
#include <memory>
#include <optional>
+#include <utility>
#include <vector>
#include "common/status.h"
@@ -35,6 +36,7 @@
namespace parquet {
class FileMetaData;
class ParquetFileReader;
+class RowGroupMetaData;
class RowGroupReader;
} // namespace parquet
@@ -93,6 +95,9 @@ Status plan_parquet_row_groups(const ::parquet::FileMetaData&
metadata,
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t
selected_rows,
int64_t batch_rows);
+uint16_t apply_compact_filter_to_selection(const IColumn::Filter& filter,
+ SelectionVector* selection,
uint16_t selected_rows);
+
Status execute_batch_filters(const format::FileScanRequest& request, int64_t
batch_rows,
Block* file_block, SelectionVector* selection,
uint16_t* selected_rows,
int64_t* conjunct_filtered_rows = nullptr);
@@ -150,7 +155,14 @@ private:
Status read_filter_columns(int64_t batch_rows, const
format::FileScanRequest& request,
Block* file_block, SelectionVector* selection,
- uint16_t* selected_rows, int64_t*
conjunct_filtered_rows);
+ uint16_t* selected_rows, int64_t*
conjunct_filtered_rows,
+ bool* predicate_columns_filtered);
+
+ Status prepare_current_dictionary_filters(
+ ParquetFileContext& file_context,
+ const std::vector<std::unique_ptr<ParquetColumnSchema>>&
file_schema,
+ const format::FileScanRequest& request, int row_group_idx,
+ const ::parquet::RowGroupMetaData& row_group_metadata);
void prefetch_current_row_group_columns(
ParquetFileContext& file_context,
@@ -178,11 +190,15 @@ private:
std::map<ColumnId, std::unique_ptr<ParquetColumnReader>>
_current_predicate_columns; // predicate ColumnReaders
std::map<ColumnId, std::unique_ptr<ParquetColumnReader>>
- _current_non_predicate_columns; // non-predicate ColumnReaders
- int64_t _current_row_group_rows = 0; // current row group row count
- int _current_row_group_id = -1; // current row group id in
parquet metadata
- int64_t _current_row_group_rows_read = 0; // rows read in the current row
group (cursor)
- int64_t _current_row_group_first_row = 0; // first file row of the current
row group
+ _current_non_predicate_columns; // non-predicate ColumnReaders
+ std::map<ColumnId, IColumn::Filter>
+ _current_dictionary_filters; // local id -> dict entry bitmap
+ std::map<ColumnId, std::vector<std::pair<VExprContextSPtr, VExprSPtr>>>
+ _current_dictionary_residual_conjuncts; // local id -> row-level
residual conjuncts
+ int64_t _current_row_group_rows = 0; // current row group row
count
+ int _current_row_group_id = -1; // current row group id in
parquet metadata
+ int64_t _current_row_group_rows_read = 0; // rows read in the
current row group (cursor)
+ int64_t _current_row_group_first_row = 0; // first file row of the
current row group
std::vector<RowRange>
_current_selected_ranges; // selected ranges for the current row
group after page-index pruning
size_t _current_range_idx = 0; // current selected_range index
diff --git a/be/src/format_v2/parquet/parquet_statistics.cpp
b/be/src/format_v2/parquet/parquet_statistics.cpp
index fa81be03e98..d0692a80205 100644
--- a/be/src/format_v2/parquet/parquet_statistics.cpp
+++ b/be/src/format_v2/parquet/parquet_statistics.cpp
@@ -498,26 +498,11 @@ bool supports_dictionary_pruning(const
ParquetColumnSchema& column_schema,
return true;
}
-struct OwnedDictionaryWords {
- std::vector<std::string> values;
- std::vector<StringRef> refs;
-
- void clear() {
- values.clear();
- refs.clear();
- }
-
- void build_refs() {
- refs.reserve(values.size());
- for (const auto& value : values) {
- refs.emplace_back(value.data(), value.size());
- }
- }
-};
+} // namespace
bool read_dictionary_words(::parquet::ParquetFileReader* file_reader, int
row_group_idx,
int leaf_column_id, const ParquetColumnSchema&
column_schema,
- OwnedDictionaryWords* dict_words) {
+ ParquetDictionaryWords* dict_words) {
DORIS_CHECK(dict_words != nullptr);
dict_words->clear();
if (file_reader == nullptr || leaf_column_id < 0) {
@@ -596,6 +581,17 @@ bool read_dictionary_words(::parquet::ParquetFileReader*
file_reader, int row_gr
return false;
}
+std::vector<Field> dictionary_fields_from_words(const ParquetDictionaryWords&
dict_words) {
+ std::vector<Field> fields;
+ fields.reserve(dict_words.refs.size());
+ for (const auto& ref : dict_words.refs) {
+ fields.push_back(Field::create_field<TYPE_STRING>(String(ref.data,
ref.size)));
+ }
+ return fields;
+}
+
+namespace {
+
const ParquetColumnSchema* resolve_local_leaf_schema(
const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema,
const format::LocalColumnId file_column_id) {
@@ -658,15 +654,6 @@ std::map<int, VExprContextSPtrs>
collect_conjuncts_by_single_slot(
return conjuncts_by_slot;
}
-std::vector<Field> dictionary_fields_from_words(const OwnedDictionaryWords&
dict_words) {
- std::vector<Field> fields;
- fields.reserve(dict_words.refs.size());
- for (const auto& ref : dict_words.refs) {
- fields.push_back(Field::create_field<TYPE_STRING>(String(ref.data,
ref.size)));
- }
- return fields;
-}
-
std::shared_ptr<segment_v2::ZoneMap> make_zonemap_from_statistics(
const ParquetColumnStatistics& statistics) {
if (!statistics.has_null_count && !statistics.has_min_max) {
@@ -785,7 +772,7 @@ ParquetRowGroupPruneReason dictionary_prune_reason(
continue;
}
- OwnedDictionaryWords dict_words;
+ ParquetDictionaryWords dict_words;
if (!read_dictionary_words(file_reader, row_group_idx,
column_schema->leaf_column_id,
*column_schema, &dict_words)) {
continue;
diff --git a/be/src/format_v2/parquet/parquet_statistics.h
b/be/src/format_v2/parquet/parquet_statistics.h
index 55da5769a6c..818d645052c 100644
--- a/be/src/format_v2/parquet/parquet_statistics.h
+++ b/be/src/format_v2/parquet/parquet_statistics.h
@@ -18,10 +18,12 @@
#include <cstdint>
#include <map>
#include <memory>
+#include <string>
#include <vector>
#include "common/status.h"
#include "core/field.h"
+#include "core/string_ref.h"
#include "exprs/vexpr_fwd.h"
#include "format_v2/file_reader.h"
#include "format_v2/parquet/selection_vector.h"
@@ -48,6 +50,36 @@ struct ParquetColumnSchema;
// ============================================================================
// ============================================================================
+struct ParquetDictionaryWords {
+ std::vector<std::string> values;
+ std::vector<StringRef> refs;
+
+ void clear() {
+ values.clear();
+ refs.clear();
+ }
+
+ void build_refs() {
+ refs.clear();
+ refs.reserve(values.size());
+ for (const auto& value : values) {
+ refs.emplace_back(value.data(), value.size());
+ }
+ }
+};
+
+// Reads the PLAIN dictionary page for BYTE_ARRAY/FIXED_LEN_BYTE_ARRAY columns
and owns copied
+// dictionary bytes in `values`. Both row-group pruning and row-level
dictionary predicates use this
+// helper so they agree on dictionary id -> Doris string value mapping.
+bool read_dictionary_words(::parquet::ParquetFileReader* file_reader, int
row_group_idx,
+ int leaf_column_id, const ParquetColumnSchema&
column_schema,
+ ParquetDictionaryWords* dict_words);
+
+std::vector<Field> dictionary_fields_from_words(const ParquetDictionaryWords&
dict_words);
+
+// ============================================================================
+// ============================================================================
+
struct ParquetPruningStats {
int64_t total_row_groups = 0; // total row groups in
the file
int64_t selected_row_groups = 0; // row groups selected
after pruning
diff --git a/be/src/format_v2/parquet/reader/column_reader.cpp
b/be/src/format_v2/parquet/reader/column_reader.cpp
index 9b7577e5521..1b6e66beefe 100644
--- a/be/src/format_v2/parquet/reader/column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/column_reader.cpp
@@ -199,6 +199,13 @@ Status ParquetColumnReader::select(const SelectionVector&
sel, uint16_t selected
return Status::OK();
}
+Status ParquetColumnReader::select_with_dictionary_filter(const
SelectionVector&, uint16_t, int64_t,
+ const
IColumn::Filter&, MutableColumnPtr&,
+ IColumn::Filter*,
bool*) {
+ return Status::NotSupported("Parquet dictionary filter is not implemented
for column {}",
+ name());
+}
+
ParquetColumnReaderFactory::ParquetColumnReaderFactory(
std::shared_ptr<::parquet::RowGroupReader> row_group, int
num_leaf_columns,
const std::map<int, ParquetPageSkipPlan>* page_skip_plans,
@@ -206,6 +213,7 @@ ParquetColumnReaderFactory::ParquetColumnReaderFactory(
bool enable_strict_mode, ParquetColumnReaderProfile
column_reader_profile)
: _row_group(std::move(row_group)),
_record_readers(static_cast<size_t>(num_leaf_columns)),
+ _dictionary_record_readers(static_cast<size_t>(num_leaf_columns)),
_page_skip_plans(page_skip_plans),
_page_skip_profile(page_skip_profile),
_timezone(timezone),
@@ -240,7 +248,7 @@ Status
ParquetColumnReaderFactory::make_scalar_column_reader(
}
Status ParquetColumnReaderFactory::create_scalar_column_reader(
- const ParquetColumnSchema& column_schema, bool is_nested,
+ const ParquetColumnSchema& column_schema, bool is_nested, bool
read_dictionary,
std::unique_ptr<ParquetColumnReader>* reader) const {
if (reader == nullptr) {
return Status::InvalidArgument("reader is null");
@@ -283,14 +291,15 @@ Status
ParquetColumnReaderFactory::create_scalar_column_reader(
// page filtering is also installed, those scratch reads can consume the
next selected row
// after a page-index range gap. Keep page filtering on flat scalar
readers only.
RETURN_IF_ERROR(get_record_reader(column_schema.leaf_column_id,
column_schema.descriptor,
- column_schema.name, !is_nested,
&record_reader));
+ column_schema.name, !is_nested,
read_dictionary,
+ &record_reader));
return make_scalar_column_reader(column_schema, std::move(record_reader),
!is_nested, reader);
}
// 1. RowGroupReader::GetColumnPageReader(leaf_column_id) -> Arrow PageReader
Status ParquetColumnReaderFactory::get_record_reader(
int leaf_column_id, const ::parquet::ColumnDescriptor* descriptor,
const std::string& name,
- bool install_page_filter,
+ bool install_page_filter, bool read_dictionary,
std::shared_ptr<::parquet::internal::RecordReader>* reader) const {
if (reader == nullptr) {
return Status::InvalidArgument("reader is null");
@@ -306,7 +315,8 @@ Status ParquetColumnReaderFactory::get_record_reader(
if (descriptor == nullptr) {
return Status::InvalidArgument("Parquet column descriptor is null for
column {}", name);
}
- if (_record_readers[leaf_column_id] == nullptr) {
+ auto& record_readers = read_dictionary ? _dictionary_record_readers :
_record_readers;
+ if (record_readers[leaf_column_id] == nullptr) {
try {
auto page_reader = _row_group->GetColumnPageReader(leaf_column_id);
if (install_page_filter) {
@@ -314,11 +324,11 @@ Status ParquetColumnReaderFactory::get_record_reader(
_page_skip_profile);
}
const auto level_info =
::parquet::internal::LevelInfo::ComputeLevelInfo(descriptor);
- _record_readers[leaf_column_id] =
::parquet::internal::RecordReader::Make(
+ record_readers[leaf_column_id] =
::parquet::internal::RecordReader::Make(
descriptor, level_info, ::arrow::default_memory_pool(),
- /*read_dictionary=*/false,
+ /*read_dictionary=*/read_dictionary,
/*read_dense_for_nullable=*/false);
-
_record_readers[leaf_column_id]->SetPageReader(std::move(page_reader));
+
record_readers[leaf_column_id]->SetPageReader(std::move(page_reader));
} catch (const ::parquet::ParquetException& e) {
return Status::Corruption("Failed to create parquet record reader
for column {}: {}",
name, e.what());
@@ -327,10 +337,10 @@ Status ParquetColumnReaderFactory::get_record_reader(
name, e.what());
}
}
- if (_record_readers[leaf_column_id] == nullptr) {
+ if (record_readers[leaf_column_id] == nullptr) {
return Status::Corruption("Failed to create parquet record reader for
column {}", name);
}
- *reader = _record_readers[leaf_column_id];
+ *reader = record_readers[leaf_column_id];
return Status::OK();
}
@@ -354,7 +364,8 @@ Status
ParquetColumnReaderFactory::create_struct_column_reader(
continue;
}
std::unique_ptr<ParquetColumnReader> child_reader;
- RETURN_IF_ERROR(create_column_reader(*child_schema, child_projection,
true, &child_reader));
+ RETURN_IF_ERROR(
+ create_column_reader(*child_schema, child_projection, true,
false, &child_reader));
child_output_indices.push_back(static_cast<int>(projected_child_types.size()));
projected_child_types.push_back(make_nullable(child_reader->type()));
projected_child_names.push_back(child_reader->name());
@@ -402,7 +413,7 @@ Status
ParquetColumnReaderFactory::create_list_column_reader(
column_schema.name);
}
RETURN_IF_ERROR(
- create_column_reader(element_schema, element_projection, true,
&element_reader));
+ create_column_reader(element_schema, element_projection, true,
false, &element_reader));
DataTypePtr type = column_schema.type;
if (format::is_partial_projection(element_projection)) {
type = std::make_shared<DataTypeArray>(element_reader->type());
@@ -447,9 +458,10 @@ Status
ParquetColumnReaderFactory::create_map_column_reader(
std::unique_ptr<ParquetColumnReader> key_reader;
// MAP materialization always needs the full key stream. It owns entry
existence, offsets and
// key equality semantics, so MAP projection is defined only as
value-subtree pruning.
- RETURN_IF_ERROR(create_column_reader(key_schema, nullptr, true,
&key_reader));
+ RETURN_IF_ERROR(create_column_reader(key_schema, nullptr, true, false,
&key_reader));
std::unique_ptr<ParquetColumnReader> value_reader;
- RETURN_IF_ERROR(create_column_reader(value_schema, value_projection, true,
&value_reader));
+ RETURN_IF_ERROR(
+ create_column_reader(value_schema, value_projection, true, false,
&value_reader));
DataTypePtr type = column_schema.type;
if (format::is_partial_projection(value_projection)) {
type = std::make_shared<DataTypeMap>(make_nullable(key_reader->type()),
@@ -466,8 +478,9 @@ Status ParquetColumnReaderFactory::create_map_column_reader(
Status ParquetColumnReaderFactory::create(const ParquetColumnSchema&
column_schema,
const format::LocalColumnIndex*
projection,
-
std::unique_ptr<ParquetColumnReader>* reader) const {
- return create_column_reader(column_schema, projection, false, reader);
+
std::unique_ptr<ParquetColumnReader>* reader,
+ bool read_dictionary) const {
+ return create_column_reader(column_schema, projection, false,
read_dictionary, reader);
}
Status ParquetColumnReaderFactory::create_count_shape_reader(
@@ -488,7 +501,7 @@ Status
ParquetColumnReaderFactory::create_count_shape_reader_impl(
return Status::InvalidArgument("Parquet COUNT projection is
invalid for column {}",
column_schema.name);
}
- return create_scalar_column_reader(column_schema, is_nested, reader);
+ return create_scalar_column_reader(column_schema, is_nested, false,
reader);
case ParquetColumnSchemaKind::STRUCT: {
if (column_schema.children.empty()) {
return Status::NotSupported("Parquet COUNT shape reader found
empty STRUCT column {}",
@@ -541,7 +554,7 @@ Status
ParquetColumnReaderFactory::create_count_shape_reader_impl(
Status ParquetColumnReaderFactory::create_column_reader(
const ParquetColumnSchema& column_schema, const
format::LocalColumnIndex* projection,
- bool is_nested, std::unique_ptr<ParquetColumnReader>* reader) const {
+ bool is_nested, bool read_dictionary,
std::unique_ptr<ParquetColumnReader>* reader) const {
if (reader == nullptr) {
return Status::InvalidArgument("reader is null");
}
@@ -552,9 +565,9 @@ Status ParquetColumnReaderFactory::create_column_reader(
return Status::InvalidArgument("Parquet scalar projection is
invalid for column {}",
column_schema.name);
}
- return create_scalar_column_reader(column_schema, true, reader);
+ return create_scalar_column_reader(column_schema, true, false,
reader);
}
- return create_scalar_column_reader(column_schema, false, reader);
+ return create_scalar_column_reader(column_schema, false,
read_dictionary, reader);
case ParquetColumnSchemaKind::STRUCT:
return create_struct_column_reader(column_schema, projection, reader);
case ParquetColumnSchemaKind::LIST:
diff --git a/be/src/format_v2/parquet/reader/column_reader.h
b/be/src/format_v2/parquet/reader/column_reader.h
index f439010e883..1d63e03ca9c 100644
--- a/be/src/format_v2/parquet/reader/column_reader.h
+++ b/be/src/format_v2/parquet/reader/column_reader.h
@@ -72,6 +72,12 @@ public:
virtual Status select(const SelectionVector& sel, uint16_t selected_rows,
int64_t batch_rows,
MutableColumnPtr& column);
+ virtual Status select_with_dictionary_filter(const SelectionVector& sel,
uint16_t selected_rows,
+ int64_t batch_rows,
+ const IColumn::Filter&
dictionary_filter,
+ MutableColumnPtr& column,
+ IColumn::Filter* row_filter,
bool* used_filter);
+
virtual Status load_nested_batch(int64_t rows);
// Shape-only load interface for COUNT(col). Implementations only
guarantee that
@@ -134,7 +140,7 @@ public:
Status create(const ParquetColumnSchema& column_schema,
const format::LocalColumnIndex* projection,
- std::unique_ptr<ParquetColumnReader>* reader) const;
+ std::unique_ptr<ParquetColumnReader>* reader, bool
read_dictionary = false) const;
// Create a scalar reader for one representative leaf that carries the
top-level column shape.
// This is used by COUNT(col): the caller needs definition/repetition
levels to decide whether
@@ -156,6 +162,7 @@ public:
private:
Status create_scalar_column_reader(const ParquetColumnSchema&
column_schema, bool is_nested,
+ bool read_dictionary,
std::unique_ptr<ParquetColumnReader>*
reader) const;
Status create_struct_column_reader(const ParquetColumnSchema&
column_schema,
@@ -172,6 +179,7 @@ private:
Status create_column_reader(const ParquetColumnSchema& column_schema,
const format::LocalColumnIndex* projection,
bool is_nested,
+ bool read_dictionary,
std::unique_ptr<ParquetColumnReader>* reader)
const;
Status create_count_shape_reader_impl(const ParquetColumnSchema&
column_schema,
const format::LocalColumnIndex*
projection,
@@ -180,6 +188,7 @@ private:
Status get_record_reader(int leaf_column_id, const
::parquet::ColumnDescriptor* descriptor,
const std::string& name, bool install_page_filter,
+ bool read_dictionary,
std::shared_ptr<::parquet::internal::RecordReader>* reader) const;
Status make_scalar_column_reader(
@@ -190,6 +199,8 @@ private:
std::shared_ptr<::parquet::RowGroupReader> _row_group; // Arrow RowGroup
reader
mutable std::vector<std::shared_ptr<::parquet::internal::RecordReader>>
_record_readers; // RecordReader cache by leaf_column_id
+ mutable std::vector<std::shared_ptr<::parquet::internal::RecordReader>>
+ _dictionary_record_readers; // dictionary-exposing RecordReader
cache by leaf_column_id
const std::map<int, ParquetPageSkipPlan>* _page_skip_plans =
nullptr; // page-index pruning
result
ParquetPageSkipProfile _page_skip_profile; // page skip profile
diff --git a/be/src/format_v2/parquet/reader/parquet_leaf_reader.cpp
b/be/src/format_v2/parquet/reader/parquet_leaf_reader.cpp
index c157ff84eef..cb59571d141 100644
--- a/be/src/format_v2/parquet/reader/parquet_leaf_reader.cpp
+++ b/be/src/format_v2/parquet/reader/parquet_leaf_reader.cpp
@@ -16,6 +16,7 @@
#include "format_v2/parquet/reader/parquet_leaf_reader.h"
#include <arrow/array/array_binary.h>
+#include <arrow/array/array_dict.h>
#include <parquet/api/schema.h>
#include <parquet/column_reader.h>
#include <parquet/exception.h>
@@ -91,6 +92,18 @@ Status decoded_fixed_value_size(const std::string&
column_name, DecodedValueKind
Status get_binary_chunks(const std::string& column_name,
::parquet::internal::RecordReader& record_reader,
std::vector<std::shared_ptr<::arrow::Array>>* chunks)
{
+ if (auto* dictionary_reader =
+
dynamic_cast<::parquet::internal::DictionaryRecordReader*>(&record_reader);
+ dictionary_reader != nullptr) {
+ auto chunked = dictionary_reader->GetResult();
+ if (chunked == nullptr) {
+ return Status::Corruption(
+ "Parquet dictionary record reader returned null result for
column {}",
+ column_name);
+ }
+ *chunks = chunked->chunks();
+ return Status::OK();
+ }
auto* binary_reader =
dynamic_cast<::parquet::internal::BinaryRecordReader*>(&record_reader);
if (binary_reader == nullptr) {
return Status::InternalError("Parquet binary record reader is not
available for column {}",
@@ -100,6 +113,53 @@ Status get_binary_chunks(const std::string& column_name,
return Status::OK();
}
+Status append_dictionary_binary_values(const std::string& column_name,
+ const ::arrow::DictionaryArray&
dictionary_array,
+ std::vector<StringRef>* values) {
+ DORIS_CHECK(values != nullptr);
+ const auto& dictionary = dictionary_array.dictionary();
+ if (dictionary == nullptr) {
+ return Status::Corruption("Parquet dictionary array has null
dictionary for column {}",
+ column_name);
+ }
+ auto append_value = [&](int64_t dictionary_index) -> Status {
+ if (dictionary_index < 0 || dictionary_index >= dictionary->length()) {
+ return Status::Corruption("Invalid parquet dictionary index {} for
column {}",
+ dictionary_index, column_name);
+ }
+ if (auto* binary_array =
dynamic_cast<::arrow::BinaryArray*>(dictionary.get())) {
+ if (binary_array->IsNull(dictionary_index)) {
+ values->emplace_back(static_cast<const char*>(nullptr), 0);
+ return Status::OK();
+ }
+ int32_t length = 0;
+ const uint8_t* value = binary_array->GetValue(dictionary_index,
&length);
+ values->emplace_back(reinterpret_cast<const char*>(value), length);
+ return Status::OK();
+ }
+ if (auto* fixed_array =
dynamic_cast<::arrow::FixedSizeBinaryArray*>(dictionary.get())) {
+ if (fixed_array->IsNull(dictionary_index)) {
+ values->emplace_back(static_cast<const char*>(nullptr), 0);
+ return Status::OK();
+ }
+ values->emplace_back(
+ reinterpret_cast<const
char*>(fixed_array->GetValue(dictionary_index)),
+ fixed_array->byte_width());
+ return Status::OK();
+ }
+ return Status::InternalError("Unexpected Arrow dictionary value array
type for column {}",
+ column_name);
+ };
+ for (int64_t row_idx = 0; row_idx < dictionary_array.length(); ++row_idx) {
+ if (dictionary_array.IsNull(row_idx)) {
+ values->emplace_back(static_cast<const char*>(nullptr), 0);
+ continue;
+ }
+ RETURN_IF_ERROR(append_value(dictionary_array.GetValueIndex(row_idx)));
+ }
+ return Status::OK();
+}
+
Status build_binary_values(const std::string& column_name,
const std::vector<std::shared_ptr<::arrow::Array>>&
chunks,
int64_t records_read, const NullMap* null_map,
@@ -131,6 +191,9 @@ Status build_binary_values(const std::string& column_name,
values->emplace_back(reinterpret_cast<const
char*>(fixed_array->GetValue(row_idx)),
fixed_array->byte_width());
}
+ } else if (auto* dictionary_array =
dynamic_cast<::arrow::DictionaryArray*>(chunk.get())) {
+ RETURN_IF_ERROR(
+ append_dictionary_binary_values(column_name,
*dictionary_array, values));
} else {
return Status::InternalError("Unexpected Arrow binary array type
for column {}",
column_name);
diff --git a/be/src/format_v2/parquet/reader/parquet_leaf_reader.h
b/be/src/format_v2/parquet/reader/parquet_leaf_reader.h
index 73b0a75e019..7d97d0ad698 100644
--- a/be/src/format_v2/parquet/reader/parquet_leaf_reader.h
+++ b/be/src/format_v2/parquet/reader/parquet_leaf_reader.h
@@ -73,6 +73,9 @@ public:
bool read_dense_for_nullable() const { return _read_dense_for_nullable; }
const int16_t* def_levels() const { return _def_levels; }
const int16_t* rep_levels() const { return _rep_levels; }
+ const std::vector<std::shared_ptr<::arrow::Array>>& binary_chunks() const {
+ return _binary_chunks;
+ }
private:
friend class ParquetLeafReader;
diff --git a/be/src/format_v2/parquet/reader/scalar_column_reader.cpp
b/be/src/format_v2/parquet/reader/scalar_column_reader.cpp
index 3c90279b441..6e3b1c7f4d5 100644
--- a/be/src/format_v2/parquet/reader/scalar_column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/scalar_column_reader.cpp
@@ -15,6 +15,8 @@
#include "format_v2/parquet/reader/scalar_column_reader.h"
+#include <arrow/array/array_binary.h>
+#include <arrow/array/array_dict.h>
#include <parquet/api/reader.h>
#include <algorithm>
@@ -23,6 +25,8 @@
#include "core/column/column.h"
#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type_serde/decoded_column_view.h"
#include "format_v2/parquet/parquet_column_schema.h"
#include "util/simd/bits.h"
@@ -79,6 +83,38 @@ Status append_scalar_batch_value(const ScalarColumnReader&
column_reader,
return Status::OK();
}
+Status append_arrow_binary_dictionary_value(const std::string& column_name,
+ const ::arrow::Array& dictionary,
+ int64_t dictionary_index,
+ std::vector<StringRef>* values) {
+ DORIS_CHECK(values != nullptr);
+ if (dictionary_index < 0 || dictionary_index >= dictionary.length()) {
+ return Status::Corruption("Invalid parquet dictionary index {} for
column {}",
+ dictionary_index, column_name);
+ }
+ if (auto* binary_array = dynamic_cast<const
::arrow::BinaryArray*>(&dictionary)) {
+ if (binary_array->IsNull(dictionary_index)) {
+ values->emplace_back(static_cast<const char*>(nullptr), 0);
+ return Status::OK();
+ }
+ int32_t length = 0;
+ const uint8_t* value = binary_array->GetValue(dictionary_index,
&length);
+ values->emplace_back(reinterpret_cast<const char*>(value), length);
+ return Status::OK();
+ }
+ if (auto* fixed_array = dynamic_cast<const
::arrow::FixedSizeBinaryArray*>(&dictionary)) {
+ if (fixed_array->IsNull(dictionary_index)) {
+ values->emplace_back(static_cast<const char*>(nullptr), 0);
+ return Status::OK();
+ }
+ values->emplace_back(reinterpret_cast<const
char*>(fixed_array->GetValue(dictionary_index)),
+ fixed_array->byte_width());
+ return Status::OK();
+ }
+ return Status::InternalError("Unexpected Arrow dictionary value array type
for column {}",
+ column_name);
+}
+
} // namespace
ScalarColumnReader::ScalarColumnReader(
@@ -215,6 +251,174 @@ Status ScalarColumnReader::skip(int64_t rows) {
return Status::OK();
}
+Status ScalarColumnReader::select_with_dictionary_filter(const
SelectionVector& sel,
+ uint16_t
selected_rows, int64_t batch_rows,
+ const
IColumn::Filter& dictionary_filter,
+ MutableColumnPtr&
column,
+ IColumn::Filter*
row_filter,
+ bool* used_filter) {
+ DORIS_CHECK(column.get() != nullptr);
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ RETURN_IF_ERROR(sel.verify(selected_rows, batch_rows));
+ *used_filter = false;
+ row_filter->clear();
+ row_filter->reserve(selected_rows);
+
+ const auto ranges = selection_to_ranges(sel, selected_rows);
+ int64_t cursor = 0;
+ for (const auto& range : ranges) {
+ if (range.start < cursor || range.start + range.length > batch_rows) {
+ return Status::InvalidArgument(
+ "Invalid parquet dictionary selection range [{}, {}) for
column {}",
+ range.start, range.start + range.length, _name);
+ }
+ RETURN_IF_ERROR(skip(range.start - cursor));
+
+ int64_t range_rows_read = 0;
+ RETURN_IF_ERROR(read_range_with_dictionary_filter(range.length,
dictionary_filter, column,
+ row_filter,
&range_rows_read,
+ used_filter));
+ if (!*used_filter) {
+ return Status::OK();
+ }
+ if (range_rows_read != range.length) {
+ return Status::Corruption(
+ "Parquet dictionary selected read returned {} rows,
expected {} rows for "
+ "column {}",
+ range_rows_read, range.length, _name);
+ }
+ cursor = range.start + range.length;
+ }
+ RETURN_IF_ERROR(skip(batch_rows - cursor));
+ if (_profile.reader_select_rows != nullptr) {
+ COUNTER_UPDATE(_profile.reader_select_rows, selected_rows);
+ }
+ return Status::OK();
+}
+
+Status ScalarColumnReader::read_range_with_dictionary_filter(
+ int64_t rows, const IColumn::Filter& dictionary_filter,
MutableColumnPtr& column,
+ IColumn::Filter* row_filter, int64_t* rows_read, bool* used_filter) {
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(rows_read != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ DORIS_CHECK(_record_reader != nullptr);
+ if (!_record_reader->read_dictionary()) {
+ *used_filter = false;
+ return Status::OK();
+ }
+
+ ParquetLeafBatch leaf_batch;
+ RETURN_IF_ERROR(leaf_reader().read_batch(rows, &leaf_batch, rows_read));
+ int64_t matched_rows = 0;
+
RETURN_IF_ERROR(append_dictionary_filtered_values(leaf_batch.binary_chunks(),
dictionary_filter,
+ column, row_filter,
&matched_rows,
+ used_filter));
+ if (!*used_filter) {
+ return Status::Corruption(
+ "Parquet dictionary reader did not return dictionary batches
for column {}", _name);
+ }
+ if (row_filter->size() < static_cast<size_t>(*rows_read)) {
+ return Status::Corruption(
+ "Parquet dictionary filter produced too few row decisions for
column {}: "
+ "filter={}, rows={}",
+ _name, row_filter->size(), *rows_read);
+ }
+ advance_rows_read(*rows_read);
+ update_reader_read_rows(*rows_read);
+ return Status::OK();
+}
+
+Status ScalarColumnReader::append_dictionary_filtered_values(
+ const std::vector<std::shared_ptr<::arrow::Array>>& chunks,
+ const IColumn::Filter& dictionary_filter, MutableColumnPtr& column,
+ IColumn::Filter* row_filter, int64_t* matched_rows, bool* used_filter)
const {
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(matched_rows != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ *matched_rows = 0;
+ *used_filter = false;
+
+ std::vector<StringRef> selected_values;
+ for (const auto& chunk : chunks) {
+ DORIS_CHECK(chunk != nullptr);
+ const auto* dict_array = dynamic_cast<const
::arrow::DictionaryArray*>(chunk.get());
+ if (dict_array == nullptr) {
+ // The caller has already consumed rows from a
DictionaryRecordReader. Falling back to a
+ // normal selected read would desynchronize the Parquet stream, so
absence of a
+ // DictionaryArray is reported as corruption by
read_range_with_dictionary_filter().
+ return Status::OK();
+ }
+ *used_filter = true;
+ const auto& dictionary = dict_array->dictionary();
+ if (dictionary == nullptr) {
+ return Status::Corruption("Parquet dictionary array has null
dictionary for column {}",
+ _name);
+ }
+
+ // Dictionary predicates are evaluated once against the dictionary
page and produce a
+ // dictionary-entry bitmap. DATA_PAGE rows then only need an
integer-index lookup. NULL rows
+ // do not have a dictionary entry and cannot satisfy the supported
equality/IN predicates.
+ for (int64_t row = 0; row < dict_array->length(); ++row) {
+ bool keep = false;
+ if (!dict_array->IsNull(row)) {
+ const int64_t dictionary_index =
dict_array->GetValueIndex(row);
+ if (dictionary_index >= 0 &&
+ dictionary_index <
static_cast<int64_t>(dictionary_filter.size())) {
+ keep =
dictionary_filter[static_cast<size_t>(dictionary_index)] != 0;
+ }
+ if (keep) {
+ RETURN_IF_ERROR(append_arrow_binary_dictionary_value(
+ _name, *dictionary, dictionary_index,
&selected_values));
+ ++*matched_rows;
+ }
+ }
+ row_filter->push_back(keep ? 1 : 0);
+ }
+ }
+
+ if (!*used_filter) {
+ return Status::OK();
+ }
+ return append_decoded_binary_values(selected_values, column);
+}
+
+Status ScalarColumnReader::append_decoded_binary_values(const
std::vector<StringRef>& values,
+ MutableColumnPtr&
column) const {
+ DecodedColumnView view;
+ view.value_kind = decoded_value_kind(_type_descriptor);
+ view.row_count = static_cast<int64_t>(values.size());
+ view.logical_integer_bit_width = _type_descriptor.integer_bit_width;
+ view.logical_integer_is_signed = !_type_descriptor.is_unsigned_integer;
+ view.fixed_length = _type_descriptor.fixed_length;
+ view.binary_values = &values;
+
+ SCOPED_TIMER(_profile.materialization_time);
+ if (!_type->is_nullable()) {
+ if (auto* nullable_column =
check_and_get_column<ColumnNullable>(*column);
+ nullable_column != nullptr) {
+ auto& nested_column = nullable_column->get_nested_column();
+ auto& null_map = nullable_column->get_null_map_data();
+ const auto old_nested_size = nested_column.size();
+ const auto old_null_map_size = null_map.size();
+ auto st =
_type->get_serde()->read_column_from_decoded_values(nested_column, view);
+ if (!st.ok()) {
+ nested_column.resize(old_nested_size);
+ return st;
+ }
+ null_map.resize(old_null_map_size + nested_column.size() -
old_nested_size);
+ memset(null_map.data() + old_null_map_size, 0, null_map.size() -
old_null_map_size);
+ return Status::OK();
+ }
+ return _type->get_serde()->read_column_from_decoded_values(*column,
view);
+ }
+
+ NullMap null_map(values.size(), 0);
+ view.null_map = null_map.empty() ? nullptr : null_map.data();
+ return _type->get_serde()->read_column_from_decoded_values(*column, view);
+}
+
// The value index stream must advance on those null slots, otherwise later
payload values shift.
Status ScalarColumnReader::load_nested_batch(int64_t rows) {
DORIS_CHECK(_nested_batch != nullptr);
diff --git a/be/src/format_v2/parquet/reader/scalar_column_reader.h
b/be/src/format_v2/parquet/reader/scalar_column_reader.h
index ab7ba0d7e54..99fec69fa3a 100644
--- a/be/src/format_v2/parquet/reader/scalar_column_reader.h
+++ b/be/src/format_v2/parquet/reader/scalar_column_reader.h
@@ -17,7 +17,9 @@
#include <memory>
#include <string>
+#include <vector>
+#include "core/string_ref.h"
#include "format_v2/parquet/parquet_type.h"
#include "format_v2/parquet/reader/column_reader.h"
#include "format_v2/parquet/reader/parquet_leaf_reader.h"
@@ -53,6 +55,11 @@ public:
Status read(int64_t rows, MutableColumnPtr& column, int64_t* rows_read)
override;
Status skip(int64_t rows) override;
+ Status select_with_dictionary_filter(const SelectionVector& sel, uint16_t
selected_rows,
+ int64_t batch_rows,
+ const IColumn::Filter&
dictionary_filter,
+ MutableColumnPtr& column,
IColumn::Filter* row_filter,
+ bool* used_filter) override;
Status load_nested_batch(int64_t rows) override;
Status load_nested_levels_batch(int64_t rows) override;
@@ -65,6 +72,15 @@ public:
private:
Status append_nested_value(int64_t level_idx, MutableColumnPtr& column)
const;
+ Status read_range_with_dictionary_filter(int64_t rows, const
IColumn::Filter& dictionary_filter,
+ MutableColumnPtr& column,
IColumn::Filter* row_filter,
+ int64_t* rows_read, bool*
used_filter);
+ Status append_dictionary_filtered_values(
+ const std::vector<std::shared_ptr<::arrow::Array>>& chunks,
+ const IColumn::Filter& dictionary_filter, MutableColumnPtr& column,
+ IColumn::Filter* row_filter, int64_t* matched_rows, bool*
used_filter) const;
+ Status append_decoded_binary_values(const std::vector<StringRef>& values,
+ MutableColumnPtr& column) const;
const ::parquet::ColumnDescriptor* descriptor() const { return
_descriptor; }
diff --git a/be/test/exprs/try_cast_expr_test.cpp
b/be/test/exprs/try_cast_expr_test.cpp
index b7ad38b2eb0..d176c6cd6c4 100644
--- a/be/test/exprs/try_cast_expr_test.cpp
+++ b/be/test/exprs/try_cast_expr_test.cpp
@@ -277,4 +277,12 @@ TEST_F(TryCastExprTest, row_exec3) {
EXPECT_FALSE(st.ok()) << st.msg();
}
-} // namespace doris
\ No newline at end of file
+TEST_F(TryCastExprTest, selected_row_safety) {
+ VCastExpr cast_expr;
+ cast_expr.add_child(std::make_shared<MockVExprForTryCast>());
+
+ EXPECT_FALSE(cast_expr.is_safe_to_execute_on_selected_rows());
+ EXPECT_TRUE(try_cast_expr.is_safe_to_execute_on_selected_rows());
+}
+
+} // namespace doris
diff --git a/be/test/format_v2/parquet/parquet_reader_test.cpp
b/be/test/format_v2/parquet/parquet_reader_test.cpp
index 8ebe266f1ac..6269a9fc2a7 100644
--- a/be/test/format_v2/parquet/parquet_reader_test.cpp
+++ b/be/test/format_v2/parquet/parquet_reader_test.cpp
@@ -48,6 +48,7 @@
#include "core/data_type/data_type_struct.h"
#include "core/data_type/primitive_type.h"
#include "core/field.h"
+#include "exprs/vcompound_pred.h"
#include "exprs/vexpr.h"
#include "exprs/vexpr_context.h"
#include "exprs/vslot_ref.h"
@@ -179,6 +180,72 @@ private:
const std::string _expr_name = "Int32SumGreaterThanExpr";
};
+class NonDeterministicCountingInt32Expr final : public VExpr {
+public:
+ NonDeterministicCountingInt32Expr(int column_id, std::vector<size_t>*
executed_rows)
+ : VExpr(std::make_shared<DataTypeUInt8>(), false),
+ _column_id(column_id),
+ _executed_rows(executed_rows) {}
+
+ Status execute_column_impl(VExprContext* context, const Block* block,
const Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ DORIS_CHECK(_executed_rows != nullptr);
+ DORIS_CHECK(block != nullptr);
+ (void)nullable_nested_column<ColumnInt32>(*block, _column_id);
+ _executed_rows->push_back(count);
+ auto result = ColumnUInt8::create();
+ result->get_data().resize_fill(count, 1);
+ result_column = std::move(result);
+ return Status::OK();
+ }
+
+ const std::string& expr_name() const override { return _expr_name; }
+
+ bool is_deterministic() const override { return false; }
+
+ void collect_slot_column_ids(std::set<int>& column_ids) const override {
+ column_ids.insert(_column_id);
+ }
+
+private:
+ const int _column_id;
+ std::vector<size_t>* const _executed_rows;
+ const std::string _expr_name = "NonDeterministicCountingInt32Expr";
+};
+
+class SelectedRowsUnsafeCountingInt32Expr final : public VExpr {
+public:
+ SelectedRowsUnsafeCountingInt32Expr(int column_id, std::vector<size_t>*
executed_rows)
+ : VExpr(std::make_shared<DataTypeUInt8>(), false),
+ _column_id(column_id),
+ _executed_rows(executed_rows) {}
+
+ Status execute_column_impl(VExprContext* context, const Block* block,
const Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ DORIS_CHECK(_executed_rows != nullptr);
+ DORIS_CHECK(block != nullptr);
+ (void)nullable_nested_column<ColumnInt32>(*block, _column_id);
+ _executed_rows->push_back(count);
+ auto result = ColumnUInt8::create();
+ result->get_data().resize_fill(count, 1);
+ result_column = std::move(result);
+ return Status::OK();
+ }
+
+ const std::string& expr_name() const override { return _expr_name; }
+
+ bool is_safe_to_execute_on_selected_rows() const override { return false; }
+
+ void collect_slot_column_ids(std::set<int>& column_ids) const override {
+ column_ids.insert(_column_id);
+ }
+
+private:
+ const int _column_id;
+ std::vector<size_t>* const _executed_rows;
+ const std::string _expr_name = "SelectedRowsUnsafeCountingInt32Expr";
+};
+
class StringInExpr final : public VExpr {
public:
StringInExpr(int column_id, std::vector<std::string> values)
@@ -232,6 +299,75 @@ private:
const std::string _expr_name = "StringInExpr";
};
+class StringEqualsExpr final : public VExpr {
+public:
+ StringEqualsExpr(int column_id, std::string row_value)
+ : VExpr(std::make_shared<DataTypeUInt8>(), false),
+ _column_id(column_id),
+ _row_value(std::move(row_value)) {}
+
+ Status execute_column_impl(VExprContext* context, const Block* block,
const Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ const auto& input = nullable_nested_column<ColumnString>(*block,
_column_id);
+ auto result = ColumnUInt8::create();
+ auto& result_data = result->get_data();
+ result_data.resize(count);
+ for (size_t row = 0; row < count; ++row) {
+ const size_t input_row = selector == nullptr ? row :
(*selector)[row];
+ result_data[row] = input.get_data_at(input_row).to_string() ==
_row_value;
+ }
+ result_column = std::move(result);
+ return Status::OK();
+ }
+
+ const std::string& expr_name() const override { return _expr_name; }
+
+ void collect_slot_column_ids(std::set<int>& column_ids) const override {
+ column_ids.insert(_column_id);
+ }
+
+private:
+ const int _column_id;
+ const std::string _row_value;
+ const std::string _expr_name = "StringEqualsExpr";
+};
+
+class StringEqualsOrLengthEqualsExpr final : public VExpr {
+public:
+ StringEqualsOrLengthEqualsExpr(int column_id, std::string row_value,
size_t length)
+ : VExpr(std::make_shared<DataTypeUInt8>(), false),
+ _column_id(column_id),
+ _row_value(std::move(row_value)),
+ _length(length) {}
+
+ Status execute_column_impl(VExprContext* context, const Block* block,
const Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ const auto& input = nullable_nested_column<ColumnString>(*block,
_column_id);
+ auto result = ColumnUInt8::create();
+ auto& result_data = result->get_data();
+ result_data.resize(count);
+ for (size_t row = 0; row < count; ++row) {
+ const size_t input_row = selector == nullptr ? row :
(*selector)[row];
+ const auto value = input.get_data_at(input_row);
+ result_data[row] = value.to_string() == _row_value || value.size
== _length;
+ }
+ result_column = std::move(result);
+ return Status::OK();
+ }
+
+ const std::string& expr_name() const override { return _expr_name; }
+
+ void collect_slot_column_ids(std::set<int>& column_ids) const override {
+ column_ids.insert(_column_id);
+ }
+
+private:
+ const int _column_id;
+ const std::string _row_value;
+ const size_t _length;
+ const std::string _expr_name = "StringEqualsOrLengthEqualsExpr";
+};
+
VExprContextSPtr create_int32_greater_than_conjunct(int column_id, int32_t
value) {
auto ctx =
VExprContext::create_shared(std::make_shared<Int32GreaterThanExpr>(column_id,
value));
@@ -249,6 +385,24 @@ VExprContextSPtr
create_int32_sum_greater_than_conjunct(int left_column_id, int
return ctx;
}
+VExprContextSPtr create_non_deterministic_counting_int32_conjunct(
+ int column_id, std::vector<size_t>* executed_rows) {
+ auto ctx = VExprContext::create_shared(
+ std::make_shared<NonDeterministicCountingInt32Expr>(column_id,
executed_rows));
+ ctx->_prepared = true;
+ ctx->_opened = true;
+ return ctx;
+}
+
+VExprContextSPtr create_selected_rows_unsafe_counting_int32_conjunct(
+ int column_id, std::vector<size_t>* executed_rows) {
+ auto ctx = VExprContext::create_shared(
+ std::make_shared<SelectedRowsUnsafeCountingInt32Expr>(column_id,
executed_rows));
+ ctx->_prepared = true;
+ ctx->_opened = true;
+ return ctx;
+}
+
VExprContextSPtr create_string_in_conjunct(int column_id,
std::vector<std::string> values) {
auto ctx = VExprContext::create_shared(
std::make_shared<StringInExpr>(column_id, std::move(values)));
@@ -257,6 +411,39 @@ VExprContextSPtr create_string_in_conjunct(int column_id,
std::vector<std::strin
return ctx;
}
+TExprNode make_compound_node(TExprOpcode::type opcode, int num_children) {
+ TExprNode node;
+ node.__set_type(create_type_desc(PrimitiveType::TYPE_BOOLEAN));
+ node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+ node.__set_opcode(opcode);
+ node.__set_num_children(num_children);
+ node.__set_is_nullable(false);
+ return node;
+}
+
+VExprContextSPtr create_string_dictionary_and_residual_conjunct(
+ int column_id, std::vector<std::string> dictionary_values, std::string
row_value) {
+ auto compound =
VCompoundPred::create_shared(make_compound_node(TExprOpcode::COMPOUND_AND, 2));
+ compound->add_child(std::make_shared<StringInExpr>(column_id,
std::move(dictionary_values)));
+ compound->add_child(std::make_shared<StringEqualsExpr>(column_id,
std::move(row_value)));
+ auto ctx = VExprContext::create_shared(std::move(compound));
+ ctx->_prepared = true;
+ ctx->_opened = true;
+ return ctx;
+}
+
+VExprContextSPtr create_nested_or_dictionary_and_residual_conjunct(int
column_id) {
+ auto root =
VCompoundPred::create_shared(make_compound_node(TExprOpcode::COMPOUND_AND, 2));
+ root->add_child(
+ std::make_shared<StringInExpr>(column_id, std::vector<std::string>
{"az", "za"}));
+
root->add_child(std::make_shared<StringEqualsOrLengthEqualsExpr>(column_id,
"az", 1));
+
+ auto ctx = VExprContext::create_shared(std::move(root));
+ ctx->_prepared = true;
+ ctx->_opened = true;
+ return ctx;
+}
+
std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) {
std::shared_ptr<arrow::Array> array;
EXPECT_TRUE(builder->Finish(&array).ok());
@@ -567,6 +754,57 @@ void write_dictionary_filter_parquet_file(const
std::string& file_path) {
builder.build()));
}
+void write_single_row_group_dictionary_filter_parquet_file(const std::string&
file_path) {
+ auto schema = arrow::schema({
+ arrow::field("id", arrow::int32(), false),
+ arrow::field("value", arrow::utf8(), false),
+ });
+ auto table =
+ arrow::Table::Make(schema, {build_int32_array({1, 2, 3, 4, 5, 6}),
+ build_string_array({"aa", "az", "lm",
"lz", "za", "zz"})});
+
+ auto file_result = arrow::io::FileOutputStream::Open(file_path);
+ ASSERT_TRUE(file_result.ok()) << file_result.status();
+ std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
+
+ ::parquet::WriterProperties::Builder builder;
+ builder.version(::parquet::ParquetVersion::PARQUET_2_6);
+ builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
+ builder.compression(::parquet::Compression::UNCOMPRESSED);
+ builder.enable_dictionary("value");
+ builder.disable_dictionary("id");
+ builder.disable_statistics();
+ PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table,
arrow::default_memory_pool(), out, 6,
+ builder.build()));
+}
+
+void write_dictionary_filter_with_trailing_column_parquet_file(const
std::string& file_path) {
+ auto schema = arrow::schema({
+ arrow::field("id", arrow::int32(), false),
+ arrow::field("value", arrow::utf8(), false),
+ arrow::field("payload", arrow::int32(), false),
+ });
+ auto table =
+ arrow::Table::Make(schema, {build_int32_array({1, 2, 3, 4, 5, 6}),
+ build_string_array({"aa", "az", "lm",
"lz", "za", "zz"}),
+ build_int32_array({10, 20, 30, 40, 50,
60})});
+
+ auto file_result = arrow::io::FileOutputStream::Open(file_path);
+ ASSERT_TRUE(file_result.ok()) << file_result.status();
+ std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
+
+ ::parquet::WriterProperties::Builder builder;
+ builder.version(::parquet::ParquetVersion::PARQUET_2_6);
+ builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
+ builder.compression(::parquet::Compression::UNCOMPRESSED);
+ builder.disable_dictionary("id");
+ builder.enable_dictionary("value");
+ builder.disable_dictionary("payload");
+ builder.disable_statistics();
+ PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table,
arrow::default_memory_pool(), out, 6,
+ builder.build()));
+}
+
void write_nested_dictionary_filter_parquet_file(const std::string& file_path)
{
auto id_field = arrow::field("id", arrow::int32(), false);
auto name_field = arrow::field("name", arrow::utf8(), false);
@@ -1459,6 +1697,91 @@ TEST_F(NewParquetReaderTest,
ReadMultiPredicateColumnsBeforeExpressionFilter) {
EXPECT_EQ(scores.get_element(1), 5);
}
+TEST_F(NewParquetReaderTest,
NonDeterministicPredicateKeepsFullBatchEvaluation) {
+ write_int_pair_parquet_file(_file_path);
+ RuntimeProfile
profile("new_parquet_reader_non_deterministic_predicate_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ Block block = build_file_block(schema);
+
+ std::vector<size_t> non_deterministic_executed_rows;
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(0), field_projection(1)};
+ request->conjuncts.push_back(create_int32_greater_than_conjunct(0, 2));
+ request->conjuncts.push_back(
+ create_non_deterministic_counting_int32_conjunct(1,
&non_deterministic_executed_rows));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_FALSE(eof);
+ ASSERT_EQ(rows, 3);
+
+ const auto& ids = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& scores = nullable_nested_column<ColumnInt32>(block, 1);
+ EXPECT_EQ(ids.get_element(0), 3);
+ EXPECT_EQ(ids.get_element(1), 4);
+ EXPECT_EQ(ids.get_element(2), 5);
+ EXPECT_EQ(scores.get_element(0), 3);
+ EXPECT_EQ(scores.get_element(1), 4);
+ EXPECT_EQ(scores.get_element(2), 5);
+
+ // A non-deterministic predicate must stay on the old full-batch path. If
it were left as a
+ // remaining conjunct while earlier deterministic predicates compacted
later predicate columns,
+ // this expression would only see the three surviving rows instead of the
original five.
+ EXPECT_EQ(non_deterministic_executed_rows,
+ std::vector<size_t>({static_cast<size_t>(ROW_COUNT)}));
+ ASSERT_NE(profile.get_counter("ReaderSelectRows"), nullptr);
+ EXPECT_EQ(profile.get_counter("ReaderSelectRows")->value(), 0);
+}
+
+TEST_F(NewParquetReaderTest,
SelectedRowsUnsafePredicateKeepsFullBatchEvaluation) {
+ write_int_pair_parquet_file(_file_path);
+ RuntimeProfile
profile("new_parquet_reader_selected_rows_unsafe_predicate_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ Block block = build_file_block(schema);
+
+ std::vector<size_t> unsafe_executed_rows;
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(0), field_projection(1)};
+ request->conjuncts.push_back(create_int32_greater_than_conjunct(0, 2));
+ request->conjuncts.push_back(
+ create_selected_rows_unsafe_counting_int32_conjunct(1,
&unsafe_executed_rows));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_FALSE(eof);
+ ASSERT_EQ(rows, 3);
+
+ const auto& ids = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& scores = nullable_nested_column<ColumnInt32>(block, 1);
+ EXPECT_EQ(ids.get_element(0), 3);
+ EXPECT_EQ(ids.get_element(1), 4);
+ EXPECT_EQ(ids.get_element(2), 5);
+ EXPECT_EQ(scores.get_element(0), 3);
+ EXPECT_EQ(scores.get_element(1), 4);
+ EXPECT_EQ(scores.get_element(2), 5);
+
+ // Error-preserving functions such as assert_true are deterministic, but
moving them after an
+ // earlier predicate's compacted selection can hide errors from rows
filtered by that earlier
+ // predicate. Such conjuncts therefore keep the old full-batch execution
path.
+ EXPECT_EQ(unsafe_executed_rows,
std::vector<size_t>({static_cast<size_t>(ROW_COUNT)}));
+ ASSERT_NE(profile.get_counter("ReaderSelectRows"), nullptr);
+ EXPECT_EQ(profile.get_counter("ReaderSelectRows")->value(), 0);
+}
+
TEST_F(NewParquetReaderTest, PredicateColumnFiltersBeforeNonPredicateRead) {
auto reader = create_reader();
RuntimeState state {TQueryOptions(), TQueryGlobals()};
@@ -1637,6 +1960,272 @@ TEST_F(NewParquetReaderTest,
PredicateFiltersRowGroupsByDictionary) {
EXPECT_EQ(values, std::vector<std::string>({"lm"}));
}
+TEST_F(NewParquetReaderTest, DictionaryPredicateFiltersRowsInsideRowGroup) {
+ write_single_row_group_dictionary_filter_parquet_file(_file_path);
+ auto parquet_file_reader =
::parquet::ParquetFileReader::OpenFile(_file_path, false);
+ ASSERT_EQ(parquet_file_reader->metadata()->num_row_groups(), 1);
+ auto row_group = parquet_file_reader->metadata()->RowGroup(0);
+ ASSERT_NE(row_group, nullptr);
+ ASSERT_TRUE(row_group->ColumnChunk(1)->has_dictionary_page());
+
+ RuntimeProfile profile("new_parquet_reader_dictionary_filter_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1)};
+ request->non_predicate_columns = {field_projection(0)};
+ request->conjuncts.push_back(create_string_in_conjunct(1, {"az", "za"}));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& value_column = nullable_nested_column<ColumnString>(block,
1);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({2, 5}));
+ EXPECT_EQ(values, std::vector<std::string>({"az", "za"}));
+ EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 4);
+ EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+ EXPECT_EQ(profile.get_counter("DictFilterCandidateColumns")->value(), 1);
+ EXPECT_EQ(profile.get_counter("DictFilterColumns")->value(), 1);
+ EXPECT_EQ(profile.get_counter("DictFilterUnsupportedColumns")->value(), 0);
+ EXPECT_EQ(profile.get_counter("DictFilterReadFailures")->value(), 0);
+ ASSERT_NE(profile.get_counter("DictFilterExprRewriteTime"), nullptr);
+ ASSERT_NE(profile.get_counter("DictFilterReadDictTime"), nullptr);
+ ASSERT_NE(profile.get_counter("DictFilterBuildTime"), nullptr);
+ EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 2);
+ EXPECT_GE(profile.get_counter("ReaderSelectRows")->value(), 8);
+}
+
+TEST_F(NewParquetReaderTest,
DictionaryPredicateProbeDoesNotUseMergeRangeReader) {
+ write_dictionary_filter_with_trailing_column_parquet_file(_file_path);
+
+ RuntimeProfile
profile("new_parquet_reader_dictionary_filter_merge_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1)};
+ request->non_predicate_columns = {field_projection(0),
field_projection(2)};
+ request->conjuncts.push_back(create_string_in_conjunct(1, {"az", "za"}));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ std::vector<int32_t> payloads;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& value_column = nullable_nested_column<ColumnString>(block,
1);
+ const auto& payload_column =
nullable_nested_column<ColumnInt32>(block, 2);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ payloads.push_back(payload_column.get_element(row));
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({2, 5}));
+ EXPECT_EQ(values, std::vector<std::string>({"az", "za"}));
+ EXPECT_EQ(payloads, std::vector<int32_t>({20, 50}));
+ EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+ ASSERT_NE(profile.get_counter("MergedIO"), nullptr);
+ ASSERT_NE(profile.get_counter("MergedBytes"), nullptr);
+}
+
+TEST_F(NewParquetReaderTest, DictionaryPredicateWorksWithoutRuntimeProfile) {
+ write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+ auto reader = create_reader();
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1)};
+ request->non_predicate_columns = {field_projection(0)};
+ request->conjuncts.push_back(create_string_in_conjunct(1, {"az", "za"}));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& value_column = nullable_nested_column<ColumnString>(block,
1);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({2, 5}));
+ EXPECT_EQ(values, std::vector<std::string>({"az", "za"}));
+}
+
+TEST_F(NewParquetReaderTest,
DictionaryPredicateSkipsRemainingPredicateColumnsWhenEmpty) {
+ write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+ RuntimeProfile
profile("new_parquet_reader_dictionary_filter_empty_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1), field_projection(0)};
+ request->conjuncts.push_back(
+ create_string_dictionary_and_residual_conjunct(1, {"az"},
"not_present"));
+ request->conjuncts.push_back(create_int32_greater_than_conjunct(0, 0));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ bool eof = false;
+ size_t total_rows = 0;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ total_rows += rows;
+ }
+
+ EXPECT_EQ(total_rows, 0);
+ EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 6);
+ EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 5);
+ EXPECT_EQ(profile.get_counter("DictFilterCandidateColumns")->value(), 1);
+ EXPECT_EQ(profile.get_counter("DictFilterColumns")->value(), 1);
+ EXPECT_EQ(profile.get_counter("DictFilterUnsupportedColumns")->value(), 0);
+ EXPECT_EQ(profile.get_counter("DictFilterReadFailures")->value(), 0);
+ EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 0);
+ // The first dictionary predicate column is read once to produce a compact
row filter. The
+ // second predicate column is skipped after the selection becomes empty,
which verifies the
+ // StarRocks-style round-by-round policy: only rows surviving previous
predicates are read.
+ EXPECT_EQ(profile.get_counter("ReaderSelectRows")->value(), 6);
+ EXPECT_EQ(profile.get_counter("ReaderSkipRows")->value(), 6);
+}
+
+TEST_F(NewParquetReaderTest,
DictionaryPredicateRunsResidualConjunctOnSurvivors) {
+ write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+ RuntimeProfile
profile("new_parquet_reader_dictionary_prefilter_residual_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1)};
+ request->non_predicate_columns = {field_projection(0)};
+ request->conjuncts.push_back(
+ create_string_dictionary_and_residual_conjunct(1, {"az", "za"},
"za"));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& value_column = nullable_nested_column<ColumnString>(block,
1);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({5}));
+ EXPECT_EQ(values, std::vector<std::string>({"za"}));
+ EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+ EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 5);
+ EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 1);
+}
+
+TEST_F(NewParquetReaderTest, DictionaryPredicateKeepsNestedOrResidualConjunct)
{
+ write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+ RuntimeProfile
profile("new_parquet_reader_dictionary_nested_or_residual_profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ request->predicate_columns = {field_projection(1)};
+ request->non_predicate_columns = {field_projection(0)};
+
request->conjuncts.push_back(create_nested_or_dictionary_and_residual_conjunct(1));
+ use_schema_order_positions(request.get(), schema);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<std::string> values;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+ const auto& value_column = nullable_nested_column<ColumnString>(block,
1);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ values.push_back(value_column.get_data_at(row).to_string());
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({2}));
+ EXPECT_EQ(values, std::vector<std::string>({"az"}));
+ EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+ EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 5);
+ EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 1);
+}
+
TEST_F(NewParquetReaderTest, ScanRangeFiltersRowGroupsBeforeDictionaryPruning)
{
write_dictionary_filter_parquet_file(_file_path);
auto parquet_file_reader =
::parquet::ParquetFileReader::OpenFile(_file_path, false);
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index 8c1ac915755..edd51c45203 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -146,11 +146,68 @@ private:
const std::string _expr_name = "Int32ZoneMapExpr";
};
+class Int32PairSumExpr final : public VExpr {
+public:
+ Int32PairSumExpr(int left_column_id, int right_column_id, int32_t
upper_bound)
+ : VExpr(std::make_shared<DataTypeUInt8>(), false),
+ _left_column_id(left_column_id),
+ _right_column_id(right_column_id),
+ _upper_bound(upper_bound) {}
+
+ const std::string& expr_name() const override { return _expr_name; }
+
+ Status execute_column_impl(VExprContext*, const Block* block, const
Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ DORIS_CHECK(block != nullptr);
+ DORIS_CHECK(selector == nullptr);
+ DORIS_CHECK(_left_column_id >= 0 && _left_column_id <
static_cast<int>(block->columns()));
+ DORIS_CHECK(_right_column_id >= 0 && _right_column_id <
static_cast<int>(block->columns()));
+ const auto& left_column =
+
int32_data_column(*block->get_by_position(_left_column_id).column);
+ const auto& right_column =
+
int32_data_column(*block->get_by_position(_right_column_id).column);
+ DORIS_CHECK(left_column.size() >= count);
+ DORIS_CHECK(right_column.size() >= count);
+
+ auto result = ColumnUInt8::create(count, 0);
+ auto& result_data = result->get_data();
+ for (size_t row = 0; row < count; ++row) {
+ result_data[row] =
+ left_column.get_element(row) +
right_column.get_element(row) < _upper_bound;
+ }
+ result_column = std::move(result);
+ return Status::OK();
+ }
+
+ void collect_slot_column_ids(std::set<int>& column_ids) const override {
+ column_ids.insert(_left_column_id);
+ column_ids.insert(_right_column_id);
+ }
+
+private:
+ int _left_column_id;
+ int _right_column_id;
+ int32_t _upper_bound;
+ const std::string _expr_name = "Int32PairSumExpr";
+};
+
VExprContextSPtr create_int32_zonemap_conjunct(int column_id,
Int32ZoneMapExpr::Op op,
int32_t value) {
return
VExprContext::create_shared(std::make_shared<Int32ZoneMapExpr>(column_id, op,
value));
}
+VExprContextSPtr create_int32_pair_sum_conjunct(int left_column_id, int
right_column_id,
+ int32_t upper_bound) {
+ return VExprContext::create_shared(
+ std::make_shared<Int32PairSumExpr>(left_column_id,
right_column_id, upper_bound));
+}
+
+int64_t counter_value(RuntimeProfile& profile, const std::string& name) {
+ auto* counter = profile.get_counter(name);
+ DORIS_CHECK(counter != nullptr);
+ return counter->value();
+}
+
std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) {
std::shared_ptr<arrow::Array> array;
EXPECT_TRUE(builder->Finish(&array).ok());
@@ -380,6 +437,23 @@ protected:
std::string _file_path;
};
+TEST(ParquetScanSelectionTest, CompactFilterShrinksCurrentSelection) {
+ format::parquet::SelectionVector selection(4);
+ selection.set_index(0, 0);
+ selection.set_index(1, 2);
+ selection.set_index(2, 4);
+ selection.set_index(3, 5);
+
+ const IColumn::Filter compact_filter {1, 0, 1, 0};
+ const auto selected_rows =
+ format::parquet::apply_compact_filter_to_selection(compact_filter,
&selection, 4);
+
+ ASSERT_EQ(selected_rows, 2);
+ EXPECT_EQ(selection.get_index(0), 0);
+ EXPECT_EQ(selection.get_index(1), 4);
+ EXPECT_TRUE(selection.verify(selected_rows, 6).ok());
+}
+
TEST_F(ParquetScanTest, PlanRowGroupsAppliesScanRangeBeforeStatistics) {
write_int_pair_parquet_file(_file_path, 2);
auto parquet_file_reader =
::parquet::ParquetFileReader::OpenFile(_file_path, false);
@@ -843,6 +917,134 @@ TEST_F(ParquetScanTest,
NoRequestedColumnsReturnsRowsOnlyAcrossRowGroups) {
EXPECT_EQ(total_rows, 6);
}
+TEST_F(ParquetScanTest, PredicateColumnsFilterRoundByRound) {
+ write_int_pair_parquet_file(_file_path, 6, false);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(1)).ok());
+ request->conjuncts.push_back(create_int32_zonemap_conjunct(0,
Int32ZoneMapExpr::Op::GT, 2));
+ request->conjuncts.push_back(create_int32_zonemap_conjunct(1,
Int32ZoneMapExpr::Op::LT, 50));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<int32_t> scores;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column =
int32_data_column(*block.get_by_position(0).column);
+ const auto& score_column =
int32_data_column(*block.get_by_position(1).column);
+ ASSERT_EQ(id_column.size(), rows);
+ ASSERT_EQ(score_column.size(), rows);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ scores.push_back(score_column.get_element(row));
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({3, 4}));
+ EXPECT_EQ(scores, std::vector<int32_t>({30, 40}));
+ EXPECT_EQ(counter_value(profile, "RawRowsRead"), 6);
+ EXPECT_EQ(counter_value(profile, "SelectedRows"), 2);
+ EXPECT_EQ(counter_value(profile, "RowsFilteredByConjunct"), 4);
+ EXPECT_EQ(counter_value(profile, "ReaderReadRows"), 10);
+ EXPECT_EQ(counter_value(profile, "ReaderSelectRows"), 4);
+ EXPECT_EQ(counter_value(profile, "ReaderSkipRows"), 2);
+}
+
+TEST_F(ParquetScanTest,
PredicateColumnsSkipUnreadColumnsWhenFirstPredicateFiltersAll) {
+ write_int_pair_parquet_file(_file_path, 6, false);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(1)).ok());
+ request->conjuncts.push_back(create_int32_zonemap_conjunct(0,
Int32ZoneMapExpr::Op::GT, 100));
+ request->conjuncts.push_back(create_int32_zonemap_conjunct(1,
Int32ZoneMapExpr::Op::LT, 50));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ size_t total_rows = 0;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ total_rows += rows;
+ }
+
+ EXPECT_EQ(total_rows, 0);
+ EXPECT_EQ(counter_value(profile, "RawRowsRead"), 6);
+ EXPECT_EQ(counter_value(profile, "SelectedRows"), 0);
+ EXPECT_EQ(counter_value(profile, "RowsFilteredByConjunct"), 6);
+ EXPECT_EQ(counter_value(profile, "EmptySelectionBatches"), 1);
+ EXPECT_EQ(counter_value(profile, "ReaderReadRows"), 6);
+ EXPECT_EQ(counter_value(profile, "ReaderSelectRows"), 0);
+ EXPECT_EQ(counter_value(profile, "ReaderSkipRows"), 6);
+}
+
+TEST_F(ParquetScanTest, MultiColumnPredicateWaitsForAllPredicateColumns) {
+ write_int_pair_parquet_file(_file_path, 6, false);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(1)).ok());
+ request->conjuncts.push_back(create_int32_pair_sum_conjunct(0, 1, 45));
+ ASSERT_TRUE(reader->open(request).ok());
+
+ std::vector<int32_t> ids;
+ std::vector<int32_t> scores;
+ bool eof = false;
+ while (!eof) {
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ if (rows == 0) {
+ continue;
+ }
+ const auto& id_column =
int32_data_column(*block.get_by_position(0).column);
+ const auto& score_column =
int32_data_column(*block.get_by_position(1).column);
+ ASSERT_EQ(id_column.size(), rows);
+ ASSERT_EQ(score_column.size(), rows);
+ for (size_t row = 0; row < rows; ++row) {
+ ids.push_back(id_column.get_element(row));
+ scores.push_back(score_column.get_element(row));
+ }
+ }
+
+ EXPECT_EQ(ids, std::vector<int32_t>({1, 2, 3, 4}));
+ EXPECT_EQ(scores, std::vector<int32_t>({10, 20, 30, 40}));
+ EXPECT_EQ(counter_value(profile, "RawRowsRead"), 6);
+ EXPECT_EQ(counter_value(profile, "SelectedRows"), 4);
+ EXPECT_EQ(counter_value(profile, "RowsFilteredByConjunct"), 2);
+ EXPECT_EQ(counter_value(profile, "ReaderReadRows"), 12);
+ EXPECT_EQ(counter_value(profile, "ReaderSelectRows"), 0);
+}
+
TEST_F(ParquetScanTest, ProfileCountersReflectPageIndexAndRangeGapPruning) {
write_page_index_parquet_file(_file_path);
RuntimeProfile profile("profile");
diff --git
a/regression-test/suites/external_table_p0/hive/test_parquet_lazy_mat_profile.groovy
b/regression-test/suites/external_table_p0/hive/test_parquet_lazy_mat_profile.groovy
index 8813c96e63c..6bbde270ba2 100644
---
a/regression-test/suites/external_table_p0/hive/test_parquet_lazy_mat_profile.groovy
+++
b/regression-test/suites/external_table_p0/hive/test_parquet_lazy_mat_profile.groovy
@@ -100,6 +100,18 @@ suite("test_parquet_lazy_mat_profile", "p0,external") {
return matcher.find() ? matcher.group(1).trim() : null
}
+ def metricValueAsLong = { String value ->
+ if (value == null) {
+ return -1L
+ }
+ def formatted = value =~ /.*\((\d+)\).*/
+ if (formatted.matches()) {
+ return formatted[0][1].toLong()
+ }
+ def plain = value.replaceAll("[^0-9-]", "")
+ return plain == "" ? -1L : plain.toLong()
+ }
+
// session vars
sql "unset variable all;"
sql "set profile_level=2;"
@@ -231,6 +243,29 @@ suite("test_parquet_lazy_mat_profile", "p0,external") {
return extractProfileBlockMetrics(profileText, "ParquetReader")
}
+ def q8 = {
+ sql """ set enable_file_scanner_v2 = true; """
+ sql """ set enable_parquet_filter_by_min_max = false; """
+ sql """ set enable_parquet_lazy_materialization = true; """
+ def t1 = UUID.randomUUID().toString()
+ def sql_result = sql """
+ select *, "${t1}" from alltypes_tiny_pages_plain where id > 2
and id < 10 order by id;
+ """
+ def idColumnIndex = 7
+ assertEquals(7, sql_result.size())
+ assertEquals("3", sql_result[0][idColumnIndex].toString())
+ assertEquals("9", sql_result[6][idColumnIndex].toString())
+
+ def profileText = getProfileWithToken(t1)
+ assertTrue(profileText.contains("ParquetReader"), "Profile does
not contain ParquetReader")
+ def metrics = extractProfileBlockMetrics(profileText,
"ParquetReader")
+ logger.info("metrics = ${metrics}")
+ assertTrue(metricValueAsLong(metrics["FilteredRowsByLazyRead"]) >
0)
+ assertTrue(metricValueAsLong(metrics["RawRowsRead"]) >= 7)
+ assertTrue(metricValueAsLong(metrics["RowsFilteredByConjunct"]) >
0)
+ assertTrue(metricValueAsLong(metrics["ReaderSelectRows"]) > 0)
+ }
+
def test_true_true = {
@@ -598,6 +633,7 @@ suite("test_parquet_lazy_mat_profile", "p0,external") {
test_true_false();
test_false_false();
test_false_true();
+ q8();
sql """drop catalog ${catalog_name};"""
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]