This is an automated email from the ASF dual-hosted git repository. Gabriel39 pushed a commit to branch refactor_0702 in repository https://gitbox.apache.org/repos/asf/doris.git
commit c5f2765706ff9829f4d070956e456a3c53be691a Author: Gabriel <[email protected]> AuthorDate: Thu Jul 2 22:00:08 2026 +0800 [fix](be) Use VExpr for file scanner v2 ZoneMap pruning ### What problem does this PR solve? Issue Number: None Related PR: None Problem Summary: File scanner v2 still used ColumnPredicate min/max and page-index statistics to prune Parquet row groups and pages. VExpr now owns ZoneMap evaluation, so the Parquet v2 pruning path builds ZoneMapEvalContext from row-group statistics and page indexes and evaluates localized VExpr conjuncts instead. ColumnPredicate pruning is kept only for dictionary and bloom filters. The existing row-group and page ZoneMap counters are preserved, page-index row-group pruning is expos [...] ### Release note None ### Check List (For Author) - Test: Manual test - Ran build-support/clang-format.sh - Ran git diff --check - Verified no file scanner v2 ColumnPredicate ZoneMap calls remain with rg - Verified no Chinese characters remain under be/src/core/data_type_serde with rg - Attempted BE UT filter `ParquetStatisticsPruningTest.*:ParquetScanTest.*:NewParquetReaderTest.*`; it did not compile/run because local thirdparty dependencies are missing (`thirdparty/installed/bin/protoc` and Snappy). - Behavior changed: No - Does this need documentation: No --- .../data_type_serde/data_type_number_serde.cpp | 10 +- be/src/core/data_type_serde/decoded_column_view.h | 5 +- be/src/format_v2/column_mapper.cpp | 5 +- be/src/format_v2/column_mapper.h | 7 +- be/src/format_v2/file_reader.h | 9 +- be/src/format_v2/parquet/parquet_profile.cpp | 13 + be/src/format_v2/parquet/parquet_profile.h | 4 + be/src/format_v2/parquet/parquet_reader.cpp | 4 +- be/src/format_v2/parquet/parquet_scan.cpp | 8 +- be/src/format_v2/parquet/parquet_scan.h | 4 +- be/src/format_v2/parquet/parquet_statistics.cpp | 271 +++++++++++++++------ be/src/format_v2/parquet/parquet_statistics.h | 17 +- be/test/format_v2/parquet/parquet_reader_test.cpp | 38 ++- be/test/format_v2/parquet/parquet_scan_test.cpp | 95 ++++++-- .../format_v2/parquet/parquet_statistics_test.cpp | 105 ++++++-- be/test/format_v2/table/iceberg_reader_test.cpp | 7 +- be/test/format_v2/table_reader_test.cpp | 7 +- 17 files changed, 453 insertions(+), 156 deletions(-) diff --git a/be/src/core/data_type_serde/data_type_number_serde.cpp b/be/src/core/data_type_serde/data_type_number_serde.cpp index 3c99a53b5b0..c4a6ae50ede 100644 --- a/be/src/core/data_type_serde/data_type_number_serde.cpp +++ b/be/src/core/data_type_serde/data_type_number_serde.cpp @@ -177,7 +177,7 @@ Status read_integer_decoded_values(IColumn& column, const DecodedColumnView& vie } } // namespace -// Type map的基本结构 +// Basic structure of the type map. template <typename Key, typename Value, typename... Rest> struct TypeMap { using KeyType = Key; @@ -185,21 +185,21 @@ struct TypeMap { using Next = TypeMap<Rest...>; }; -// Type map的末端 +// End marker of the type map. template <> struct TypeMap<void, void> {}; -// TypeMapLookup 前向声明 +// Forward declaration of TypeMapLookup. template <typename Key, typename Map> struct TypeMapLookup; -// Type map查找:找到匹配的键时的情况 +// Type map lookup when the key matches. template <typename Key, typename Value, typename... Rest> struct TypeMapLookup<Key, TypeMap<Key, Value, Rest...>> { using ValueType = Value; }; -// Type map查找:递归查找 +// Type map lookup by recursive search. template <typename Key, typename K, typename V, typename... Rest> struct TypeMapLookup<Key, TypeMap<K, V, Rest...>> { using ValueType = typename TypeMapLookup<Key, TypeMap<Rest...>>::ValueType; diff --git a/be/src/core/data_type_serde/decoded_column_view.h b/be/src/core/data_type_serde/decoded_column_view.h index 9f40f1513db..62efd2ca466 100644 --- a/be/src/core/data_type_serde/decoded_column_view.h +++ b/be/src/core/data_type_serde/decoded_column_view.h @@ -33,8 +33,9 @@ namespace doris { class IColumn; -// 已解码 column batch 的物理值来源类型。 -// 该枚举只描述通用内存布局,不包含 Parquet/ORC/Arrow 等格式专有类型。 +// Physical value source type for a decoded column batch. +// This enum describes only generic memory layouts, not format-specific types such as +// Parquet/ORC/Arrow. enum class DecodedValueKind { BOOL, INT32, diff --git a/be/src/format_v2/column_mapper.cpp b/be/src/format_v2/column_mapper.cpp index 4a76b112456..896f144b881 100644 --- a/be/src/format_v2/column_mapper.cpp +++ b/be/src/format_v2/column_mapper.cpp @@ -1710,8 +1710,9 @@ Status TableColumnMapper::create_scan_request( auto* mapping = _find_mapping(global_index); if (mapping != nullptr && mapping->file_local_id.has_value()) { // A file column can be read lazily as a non-predicate column only when it is not used - // by row-level expression filters. Single-column ColumnPredicate filters are pruning - // hints only and must not force row-level predicate materialization. + // by row-level expression filters. Single-column ColumnPredicate filters are + // dictionary/bloom pruning hints only and must not force row-level predicate + // materialization. bool used_by_filter = false; for (const auto& table_filter : table_filters) { const auto& global_indices = table_filter.global_indices; diff --git a/be/src/format_v2/column_mapper.h b/be/src/format_v2/column_mapper.h index 77c444ba0f1..acda5467bd5 100644 --- a/be/src/format_v2/column_mapper.h +++ b/be/src/format_v2/column_mapper.h @@ -225,7 +225,7 @@ protected: // should keep all required columns in one scan list. virtual bool enable_lazy_materialization() const { return true; } // File-layer column predicate filters are reader-specific pruning hints. Parquet consumes them - // for row-group/page-index/statistics pruning; simple delimited readers do not. + // for dictionary and bloom-filter pruning; ZoneMap pruning uses file-local VExpr conjuncts. virtual bool enable_column_predicate_filters() const { return true; } // Row-oriented readers such as CSV/Text cannot physically read only a nested child from one // delimited text field. They must scan the whole complex top-level field and let TableReader @@ -271,8 +271,9 @@ protected: std::map<std::string, Field> _partition_values; }; -// Parquet consumes the full FileScanRequest shape: predicate columns for lazy materialization and -// top-level column_predicate_filters for statistics/page-index pruning. +// Parquet consumes the full FileScanRequest shape: predicate columns for lazy materialization, +// file-local conjuncts for ZoneMap pruning, and top-level column_predicate_filters for +// dictionary/bloom pruning. class ParquetColumnMapper final : public TableColumnMapper { public: using TableColumnMapper::TableColumnMapper; diff --git a/be/src/format_v2/file_reader.h b/be/src/format_v2/file_reader.h index e4c55bd9a6b..27aceeae944 100644 --- a/be/src/format_v2/file_reader.h +++ b/be/src/format_v2/file_reader.h @@ -48,8 +48,9 @@ namespace doris::format { class TableColumnMapper; struct TableColumnMapperOptions; -// File-local single-column predicates for file-layer pruning, such as min/max, page index, -// dictionary and bloom filter. Predicates must all belong to file_column_id. +// File-local single-column predicates for file-layer pruning, such as dictionary and bloom filter. +// ZoneMap pruning uses file-local VExpr conjuncts instead. Predicates must all belong to +// file_column_id. // These predicates are pruning hints only and are not row-level conjuncts. struct FileColumnPredicateFilter { LocalColumnId file_column_id = LocalColumnId::invalid(); @@ -86,8 +87,8 @@ struct FileScanRequest { VExprContextSPtrs conjuncts; // Delete predicates converted to file-local expressions. VExprContextSPtrs delete_conjuncts; - // Single-column predicates used only for file-layer pruning, such as statistics, page index, - // dictionary and bloom filter. They must not be used for batch row-level filtering. + // Single-column predicates used only for file-layer pruning, such as dictionary and bloom + // filter. They must not be used for batch row-level filtering. std::vector<FileColumnPredicateFilter> column_predicate_filters; }; diff --git a/be/src/format_v2/parquet/parquet_profile.cpp b/be/src/format_v2/parquet/parquet_profile.cpp index 79f979ea0cf..4bf7af687f2 100644 --- a/be/src/format_v2/parquet/parquet_profile.cpp +++ b/be/src/format_v2/parquet/parquet_profile.cpp @@ -37,6 +37,8 @@ void ParquetProfile::init(RuntimeProfile* profile) { profile, "RowGroupsFilteredByDictionary", TUnit::UNIT, parquet_profile, 1); filtered_row_groups_by_bloom_filter = ADD_CHILD_COUNTER_WITH_LEVEL( profile, "RowGroupsFilteredByBloomFilter", TUnit::UNIT, parquet_profile, 1); + filtered_row_groups_by_page_index = ADD_CHILD_COUNTER_WITH_LEVEL( + profile, "RowGroupsFilteredByPageIndex", TUnit::UNIT, parquet_profile, 1); to_read_row_groups = ADD_CHILD_COUNTER_WITH_LEVEL(profile, "RowGroupsReadNum", TUnit::UNIT, parquet_profile, 1); total_row_groups = ADD_CHILD_COUNTER_WITH_LEVEL(profile, "RowGroupsTotalNum", TUnit::UNIT, @@ -91,6 +93,12 @@ void ParquetProfile::init(RuntimeProfile* profile) { ADD_CHILD_TIMER_WITH_LEVEL(profile, "PageIndexReadTime", parquet_profile, 1); parse_page_index_time = ADD_CHILD_TIMER_WITH_LEVEL(profile, "PageIndexParseTime", parquet_profile, 1); + expr_zonemap_unusable = ADD_CHILD_COUNTER_WITH_LEVEL(profile, "ExprZoneMapUnusableEvals", + TUnit::UNIT, parquet_profile, 1); + in_zonemap_point_check = ADD_CHILD_COUNTER_WITH_LEVEL(profile, "InZoneMapPointCheckCount", + TUnit::UNIT, parquet_profile, 1); + in_zonemap_range_only = ADD_CHILD_COUNTER_WITH_LEVEL(profile, "InZoneMapRangeOnlyCount", + TUnit::UNIT, parquet_profile, 1); row_group_filter_time = ADD_CHILD_TIMER_WITH_LEVEL(profile, "RowGroupFilterTime", parquet_profile, 1); file_footer_read_calls = ADD_COUNTER_WITH_LEVEL(profile, "FileFooterReadCalls", TUnit::UNIT, 1); @@ -144,6 +152,8 @@ void ParquetProfile::update_pruning_stats(const ParquetPruningStats& pruning_sta pruning_stats.filtered_row_groups_by_dictionary); COUNTER_UPDATE(filtered_row_groups_by_bloom_filter, pruning_stats.filtered_row_groups_by_bloom_filter); + COUNTER_UPDATE(filtered_row_groups_by_page_index, + pruning_stats.filtered_row_groups_by_page_index); COUNTER_UPDATE(to_read_row_groups, pruning_stats.selected_row_groups); COUNTER_UPDATE(total_row_groups, pruning_stats.total_row_groups); COUNTER_UPDATE(selected_row_ranges, pruning_stats.selected_row_ranges); @@ -154,6 +164,9 @@ void ParquetProfile::update_pruning_stats(const ParquetPruningStats& pruning_sta COUNTER_UPDATE(row_group_filter_time, pruning_stats.row_group_filter_time); COUNTER_UPDATE(page_index_filter_time, pruning_stats.page_index_filter_time); COUNTER_UPDATE(read_page_index_time, pruning_stats.read_page_index_time); + COUNTER_UPDATE(expr_zonemap_unusable, pruning_stats.expr_zonemap_unusable_evals); + COUNTER_UPDATE(in_zonemap_point_check, pruning_stats.in_zonemap_point_check_count); + COUNTER_UPDATE(in_zonemap_range_only, pruning_stats.in_zonemap_range_only_count); } ParquetPageSkipProfile ParquetProfile::page_skip_profile() const { diff --git a/be/src/format_v2/parquet/parquet_profile.h b/be/src/format_v2/parquet/parquet_profile.h index 8f7623527ca..91c8ad89ddc 100644 --- a/be/src/format_v2/parquet/parquet_profile.h +++ b/be/src/format_v2/parquet/parquet_profile.h @@ -70,6 +70,7 @@ struct ParquetProfile { RuntimeProfile::Counter* filtered_row_groups_by_min_max = nullptr; RuntimeProfile::Counter* filtered_row_groups_by_dictionary = nullptr; RuntimeProfile::Counter* filtered_row_groups_by_bloom_filter = nullptr; + RuntimeProfile::Counter* filtered_row_groups_by_page_index = nullptr; RuntimeProfile::Counter* to_read_row_groups = nullptr; RuntimeProfile::Counter* total_row_groups = nullptr; RuntimeProfile::Counter* selected_row_ranges = nullptr; @@ -110,6 +111,9 @@ struct ParquetProfile { RuntimeProfile::Counter* page_index_filter_time = nullptr; RuntimeProfile::Counter* read_page_index_time = nullptr; RuntimeProfile::Counter* parse_page_index_time = nullptr; + RuntimeProfile::Counter* expr_zonemap_unusable = nullptr; + RuntimeProfile::Counter* in_zonemap_point_check = nullptr; + RuntimeProfile::Counter* in_zonemap_range_only = nullptr; RuntimeProfile::Counter* decompress_time = nullptr; RuntimeProfile::Counter* decompress_cnt = nullptr; diff --git a/be/src/format_v2/parquet/parquet_reader.cpp b/be/src/format_v2/parquet/parquet_reader.cpp index 3e13333e7d4..50ccc9f29d8 100644 --- a/be/src/format_v2/parquet/parquet_reader.cpp +++ b/be/src/format_v2/parquet/parquet_reader.cpp @@ -48,6 +48,7 @@ struct ParquetReaderScanState { std::vector<std::unique_ptr<ParquetColumnSchema>> file_schema; RowGroupScanPlan scan_plan; ParquetScanScheduler scheduler; + const RuntimeState* runtime_state = nullptr; const cctz::time_zone* timezone = nullptr; bool enable_bloom_filter = false; bool enable_page_cache = false; @@ -329,6 +330,7 @@ Status ParquetReader::init(RuntimeState* state) { _state->enable_page_cache = state != nullptr && state->query_options().enable_parquet_file_page_cache; if (state != nullptr) { + _state->runtime_state = state; _state->timezone = &state->timezone_obj(); _state->enable_strict_mode = state->enable_strict_mode(); _state->scheduler.set_timezone(&state->timezone_obj()); @@ -443,7 +445,7 @@ Status ParquetReader::open(std::shared_ptr<format::FileScanRequest> request) { RETURN_IF_ERROR(plan_parquet_row_groups( *_state->file_context.metadata, _state->file_context.file_reader.get(), _state->file_schema, *request_snapshot, scan_range, _state->enable_bloom_filter, - &row_group_plan, _state->timezone)); + &row_group_plan, _state->timezone, _state->runtime_state)); if (_profile != nullptr) { _parquet_profile.update_pruning_stats(row_group_plan.pruning_stats); } diff --git a/be/src/format_v2/parquet/parquet_scan.cpp b/be/src/format_v2/parquet/parquet_scan.cpp index d636f3e3f9e..bc391ba09c9 100644 --- a/be/src/format_v2/parquet/parquet_scan.cpp +++ b/be/src/format_v2/parquet/parquet_scan.cpp @@ -76,7 +76,8 @@ Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata, const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, const ParquetScanRange& scan_range, bool enable_bloom_filter, - RowGroupScanPlan* plan, const cctz::time_zone* timezone) { + RowGroupScanPlan* plan, const cctz::time_zone* timezone, + const RuntimeState* runtime_state) { DORIS_CHECK(plan != nullptr); plan->row_groups.clear(); plan->pruning_stats = ParquetPruningStats {}; @@ -103,7 +104,8 @@ Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata, std::vector<int> statistics_selected_row_groups; RETURN_IF_ERROR(select_row_groups_by_statistics( metadata, file_reader, file_schema, request, &scan_range_selected_row_groups, - &statistics_selected_row_groups, enable_bloom_filter, &plan->pruning_stats, timezone)); + &statistics_selected_row_groups, enable_bloom_filter, &plan->pruning_stats, timezone, + runtime_state)); plan->row_groups.reserve(statistics_selected_row_groups.size()); for (const auto row_group_idx : statistics_selected_row_groups) { @@ -121,7 +123,7 @@ Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata, RETURN_IF_ERROR(select_row_group_ranges_by_page_index( file_reader, file_schema, request, row_group_idx, row_group_rows, &row_group_plan.selected_ranges, &row_group_plan.page_skip_plans, - &plan->pruning_stats, timezone)); + &plan->pruning_stats, timezone, runtime_state)); if (row_group_plan.selected_ranges.empty()) { continue; } diff --git a/be/src/format_v2/parquet/parquet_scan.h b/be/src/format_v2/parquet/parquet_scan.h index ca3c665a2e9..e6477f9f34e 100644 --- a/be/src/format_v2/parquet/parquet_scan.h +++ b/be/src/format_v2/parquet/parquet_scan.h @@ -44,6 +44,7 @@ class time_zone; namespace doris { class Block; +class RuntimeState; namespace format { struct FileScanRequest; @@ -86,7 +87,8 @@ Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata, const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, const ParquetScanRange& scan_range, bool enable_bloom_filter, - RowGroupScanPlan* plan, const cctz::time_zone* timezone = nullptr); + RowGroupScanPlan* plan, const cctz::time_zone* timezone = nullptr, + const RuntimeState* runtime_state = nullptr); IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t selected_rows, int64_t batch_rows); diff --git a/be/src/format_v2/parquet/parquet_statistics.cpp b/be/src/format_v2/parquet/parquet_statistics.cpp index 66af8515aba..918017f501b 100644 --- a/be/src/format_v2/parquet/parquet_statistics.cpp +++ b/be/src/format_v2/parquet/parquet_statistics.cpp @@ -30,6 +30,7 @@ #include <exception> #include <map> #include <memory> +#include <optional> #include <set> #include <string> #include <utility> @@ -40,9 +41,12 @@ #include "core/data_type/data_type_nullable.h" #include "core/data_type_serde/data_type_serde.h" #include "core/field.h" +#include "exprs/expr_zonemap_filter.h" +#include "exprs/vexpr_context.h" #include "format_v2/parquet/parquet_column_schema.h" #include "runtime/runtime_profile.h" #include "storage/index/zone_map/zone_map_index.h" +#include "storage/index/zone_map/zonemap_eval_context.h" #include "storage/predicate/accept_null_predicate.h" #include "storage/predicate/column_predicate.h" @@ -52,7 +56,6 @@ namespace { enum class ParquetRowGroupPruneReason { NONE, // cannot prune; must read - STATISTICS, // excluded by min/max statistics DICTIONARY, // excluded by dictionary BLOOM_FILTER, // excluded by bloom filter }; @@ -586,15 +589,6 @@ bool read_dictionary_words(::parquet::ParquetFileReader* file_reader, int row_gr return false; } -segment_v2::ZoneMap to_column_predicate_statistics(const ParquetColumnStatistics& statistics) { - segment_v2::ZoneMap predicate_statistics; - predicate_statistics.min_value = statistics.min_value; - predicate_statistics.max_value = statistics.max_value; - predicate_statistics.has_null = statistics.has_null; - predicate_statistics.has_not_null = statistics.has_not_null; - return predicate_statistics; -} - const ParquetColumnSchema* resolve_predicate_leaf_schema( const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema, const format::FileColumnPredicateFilter& column_filter) { @@ -613,27 +607,92 @@ const ParquetColumnSchema* resolve_predicate_leaf_schema( return column_schema; } -bool check_statistics(const format::FileColumnPredicateFilter& column_filter, - const ParquetColumnStatistics& statistics) { - if (!statistics.has_any_statistics()) { - return false; +const ParquetColumnSchema* resolve_local_leaf_schema( + const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema, + const format::LocalColumnId file_column_id) { + if (!file_column_id.is_valid() || file_column_id.value() >= static_cast<int>(schema.size())) { + return nullptr; } + const ParquetColumnSchema* column_schema = schema[file_column_id.value()].get(); + if (column_schema == nullptr || column_schema->kind != ParquetColumnSchemaKind::PRIMITIVE || + column_schema->leaf_column_id < 0 || column_schema->max_repetition_level > 0) { + return nullptr; + } + return column_schema; +} - for (const auto& column_predicate : column_filter.predicates) { - if (is_null_only_predicate(*column_predicate)) { - if (!statistics.has_null_count) { - continue; - } - } else if (!statistics.has_any_statistics()) { - continue; +std::optional<format::LocalColumnId> file_column_id_by_block_position( + const format::FileScanRequest& request, int block_position) { + for (const auto& [file_column_id, local_index] : request.local_positions) { + if (local_index.value() == block_position) { + return file_column_id; } - if (!column_predicate->evaluate_and(to_column_predicate_statistics(statistics))) { + } + return std::nullopt; +} + +bool has_expr_zonemap_filter(const format::FileScanRequest& request, + const RuntimeState* runtime_state) { + if (!expr_zonemap::is_expr_zonemap_filter_enabled(runtime_state)) { + return false; + } + for (const auto& conjunct : request.conjuncts) { + if (conjunct != nullptr && conjunct->root() != nullptr && + conjunct->root()->can_evaluate_zonemap_filter()) { return true; } } return false; } +std::set<int> collect_expr_zonemap_slot_indexes(const VExprContextSPtrs& conjuncts) { + std::set<int> slot_indexes; + for (const auto& conjunct : conjuncts) { + if (conjunct != nullptr && conjunct->root() != nullptr && + conjunct->root()->can_evaluate_zonemap_filter()) { + conjunct->root()->collect_slot_column_ids(slot_indexes); + } + } + return slot_indexes; +} + +std::shared_ptr<segment_v2::ZoneMap> make_zonemap_from_statistics( + const ParquetColumnStatistics& statistics) { + if (!statistics.has_null_count && !statistics.has_min_max) { + return nullptr; + } + segment_v2::ZoneMap zone_map; + zone_map.has_null = statistics.has_null; + zone_map.has_not_null = statistics.has_not_null; + if (!statistics.has_not_null) { + return std::make_shared<segment_v2::ZoneMap>(std::move(zone_map)); + } + if (!statistics.has_min_max) { + return nullptr; + } + zone_map.min_value = statistics.min_value; + zone_map.max_value = statistics.max_value; + return std::make_shared<segment_v2::ZoneMap>(std::move(zone_map)); +} + +void add_slot_zonemap(ZoneMapEvalContext* ctx, int slot_index, const DataTypePtr& data_type, + std::shared_ptr<segment_v2::ZoneMap> zone_map) { + DORIS_CHECK(ctx != nullptr); + ZoneMapEvalContext::SlotZoneMap slot_zone_map; + slot_zone_map.data_type = data_type; + slot_zone_map.zone_map = std::move(zone_map); + ctx->slots.emplace(slot_index, std::move(slot_zone_map)); +} + +void accumulate_zonemap_stats(const ZoneMapEvalContext& ctx, ParquetPruningStats* pruning_stats) { + if (pruning_stats == nullptr) { + return; + } + pruning_stats->expr_zonemap_unusable_evals += ctx.stats.unusable_zonemap_eval_count; + pruning_stats->in_zonemap_point_check_count += ctx.stats.in_zonemap_point_check_count; + pruning_stats->in_zonemap_range_only_count += ctx.stats.in_zonemap_range_only_count; +} + } // namespace ParquetColumnStatistics ParquetStatisticsUtils::TransformColumnStatistics( @@ -704,11 +763,6 @@ ParquetRowGroupPruneReason row_group_prune_reason( if (column_chunk == nullptr) { return ParquetRowGroupPruneReason::NONE; } - if (check_statistics(column_filter, - ParquetStatisticsUtils::TransformColumnStatistics( - *column_schema, column_chunk->statistics(), timezone))) { - return ParquetRowGroupPruneReason::STATISTICS; - } if (!supports_dictionary_pruning(*column_schema, *column_chunk, column_filter) || !is_dictionary_encoded_chunk(*column_chunk)) { return bloom_filter_prune_reason(row_group_idx, schema, column_filter, bloom_filter_cache, @@ -744,13 +798,51 @@ void init_bloom_filter_cache(::parquet::ParquetFileReader* file_reader, bool ena } } +bool row_group_pruned_by_expr_zonemap( + const ::parquet::RowGroupMetaData& row_group, + const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, + const format::FileScanRequest& request, ParquetPruningStats* pruning_stats, + const cctz::time_zone* timezone) { + const auto slot_indexes = collect_expr_zonemap_slot_indexes(request.conjuncts); + if (slot_indexes.empty()) { + return false; + } + + ZoneMapEvalContext ctx; + for (const int slot_index : slot_indexes) { + const auto file_column_id = file_column_id_by_block_position(request, slot_index); + if (!file_column_id.has_value()) { + continue; + } + const auto* column_schema = resolve_local_leaf_schema(file_schema, *file_column_id); + if (column_schema == nullptr || column_schema->type == nullptr) { + continue; + } + + std::shared_ptr<segment_v2::ZoneMap> zone_map; + DCHECK_LT(column_schema->leaf_column_id, row_group.num_columns()); + auto column_chunk = row_group.ColumnChunk(column_schema->leaf_column_id); + if (column_chunk != nullptr) { + zone_map = + make_zonemap_from_statistics(ParquetStatisticsUtils::TransformColumnStatistics( + *column_schema, column_chunk->statistics(), timezone)); + } + add_slot_zonemap(&ctx, slot_index, column_schema->type, std::move(zone_map)); + } + + const auto result = VExprContext::evaluate_zonemap_filter(request.conjuncts, ctx); + accumulate_zonemap_stats(ctx, pruning_stats); + return result == ZoneMapFilterResult::kNoMatch; +} + Status select_row_groups(const ::parquet::FileMetaData& metadata, ::parquet::ParquetFileReader* file_reader, const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, const std::vector<int>* candidate_row_groups, std::vector<int>* selected_row_groups, bool enable_bloom_filter, - ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone) { + ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone, + const RuntimeState* runtime_state) { int64_t row_group_filter_time_sink = 0; SCOPED_RAW_TIMER(pruning_stats == nullptr ? &row_group_filter_time_sink : &pruning_stats->row_group_filter_time); @@ -778,6 +870,15 @@ Status select_row_groups(const ::parquet::FileMetaData& metadata, selected_row_groups->push_back(row_group_idx); continue; } + if (has_expr_zonemap_filter(request, runtime_state) && + row_group_pruned_by_expr_zonemap(*row_group, file_schema, request, pruning_stats, + timezone)) { + if (pruning_stats != nullptr) { + pruning_stats->filtered_group_rows += row_group->num_rows(); + ++pruning_stats->filtered_row_groups_by_statistics; + } + continue; + } bool drop = false; RowGroupBloomFilterCache bloom_filter_cache; init_bloom_filter_cache(file_reader, enable_bloom_filter, &bloom_filter_cache); @@ -791,9 +892,7 @@ Status select_row_groups(const ::parquet::FileMetaData& metadata, drop = true; if (pruning_stats != nullptr) { pruning_stats->filtered_group_rows += row_group->num_rows(); - if (prune_reason == ParquetRowGroupPruneReason::STATISTICS) { - ++pruning_stats->filtered_row_groups_by_statistics; - } else if (prune_reason == ParquetRowGroupPruneReason::DICTIONARY) { + if (prune_reason == ParquetRowGroupPruneReason::DICTIONARY) { ++pruning_stats->filtered_row_groups_by_dictionary; } else if (prune_reason == ParquetRowGroupPruneReason::BLOOM_FILTER) { ++pruning_stats->filtered_row_groups_by_bloom_filter; @@ -824,9 +923,11 @@ Status select_row_groups_by_statistics( const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, const std::vector<int>* candidate_row_groups, std::vector<int>* selected_row_groups, bool enable_bloom_filter, - ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone) { + ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone, + const RuntimeState* runtime_state) { return select_row_groups(metadata, file_reader, file_schema, request, candidate_row_groups, - selected_row_groups, enable_bloom_filter, pruning_stats, timezone); + selected_row_groups, enable_bloom_filter, pruning_stats, timezone, + runtime_state); } namespace { @@ -1024,35 +1125,58 @@ void append_row_range(const RowRange& range, std::vector<RowRange>* ranges) { ranges->push_back(range); } -bool select_ranges_for_filter(const std::shared_ptr<::parquet::RowGroupPageIndexReader>& row_group, - const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, - const format::FileColumnPredicateFilter& column_filter, - int64_t row_group_rows, std::vector<RowRange>* ranges, - const cctz::time_zone* timezone) { - if (column_filter.predicates.empty()) { - return false; +std::optional< + std::pair<std::shared_ptr<::parquet::ColumnIndex>, std::shared_ptr<::parquet::OffsetIndex>>> +load_page_indexes_for_slot(const std::shared_ptr<::parquet::RowGroupPageIndexReader>& row_group, + const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, + const format::FileScanRequest& request, int slot_index, + const ParquetColumnSchema** column_schema) { + DORIS_CHECK(column_schema != nullptr); + *column_schema = nullptr; + const auto file_column_id = file_column_id_by_block_position(request, slot_index); + if (!file_column_id.has_value()) { + return std::nullopt; } - const auto* column_schema = resolve_predicate_leaf_schema(file_schema, column_filter); - if (column_schema == nullptr || column_schema->descriptor == nullptr) { - return false; + *column_schema = resolve_local_leaf_schema(file_schema, *file_column_id); + if (*column_schema == nullptr || (*column_schema)->descriptor == nullptr) { + return std::nullopt; } - std::shared_ptr<::parquet::ColumnIndex> column_index; - std::shared_ptr<::parquet::OffsetIndex> offset_index; try { - column_index = row_group->GetColumnIndex(column_schema->leaf_column_id); - offset_index = row_group->GetOffsetIndex(column_schema->leaf_column_id); + auto column_index = row_group->GetColumnIndex((*column_schema)->leaf_column_id); + auto offset_index = row_group->GetOffsetIndex((*column_schema)->leaf_column_id); + if (column_index == nullptr || offset_index == nullptr || + column_index->null_pages().size() != offset_index->page_locations().size()) { + return std::nullopt; + } + return std::make_pair(std::move(column_index), std::move(offset_index)); } catch (const ::parquet::ParquetException&) { - return false; + return std::nullopt; } catch (const std::exception&) { + return std::nullopt; + } +} + +bool select_ranges_for_expr_zonemap( + const std::shared_ptr<::parquet::RowGroupPageIndexReader>& row_group, + const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, + const format::FileScanRequest& request, int slot_index, const VExprContextSPtrs& conjuncts, + int64_t row_group_rows, std::vector<RowRange>* ranges, ParquetPruningStats* pruning_stats, + const cctz::time_zone* timezone) { + DORIS_CHECK(ranges != nullptr); + if (conjuncts.empty()) { return false; } - if (column_index == nullptr || offset_index == nullptr || - column_index->null_pages().size() != offset_index->page_locations().size()) { + const ParquetColumnSchema* column_schema = nullptr; + const auto page_indexes = + load_page_indexes_for_slot(row_group, file_schema, request, slot_index, &column_schema); + if (!page_indexes.has_value()) { return false; } + const auto& [column_index, offset_index] = *page_indexes; ranges->clear(); + ZoneMapEvalStats page_stats; const auto page_count = offset_index->page_locations().size(); for (size_t page_idx = 0; page_idx < page_count; ++page_idx) { ParquetColumnStatistics page_statistics; @@ -1061,11 +1185,21 @@ bool select_ranges_for_filter(const std::shared_ptr<::parquet::RowGroupPageIndex ranges->clear(); return false; } - const RowRange row_range = page_row_range(*offset_index, page_idx, row_group_rows); - if (check_statistics(column_filter, page_statistics)) { + + ZoneMapEvalContext ctx; + add_slot_zonemap(&ctx, slot_index, column_schema->type, + make_zonemap_from_statistics(page_statistics)); + const auto result = VExprContext::evaluate_zonemap_filter(conjuncts, ctx); + page_stats.merge_page_eval_stats(ctx.stats); + if (result == ZoneMapFilterResult::kNoMatch) { continue; } - append_row_range(row_range, ranges); + append_row_range(page_row_range(*offset_index, page_idx, row_group_rows), ranges); + } + if (pruning_stats != nullptr) { + pruning_stats->expr_zonemap_unusable_evals += page_stats.unusable_zonemap_eval_count; + pruning_stats->in_zonemap_point_check_count += page_stats.in_zonemap_point_check_count; + pruning_stats->in_zonemap_range_only_count += page_stats.in_zonemap_range_only_count; } return true; } @@ -1127,15 +1261,6 @@ void collect_request_leaf_schemas( for (const auto& projection : request.non_predicate_columns) { collect_projection(projection); } - for (const auto& column_filter : request.column_predicate_filters) { - const auto* leaf_schema = resolve_predicate_leaf_schema(file_schema, column_filter); - if (leaf_schema == nullptr) { - continue; - } - if (seen_leaf_ids.insert(leaf_schema->leaf_column_id).second) { - leaf_schemas->push_back(leaf_schema); - } - } } bool build_page_skip_plan_for_leaf( @@ -1144,8 +1269,6 @@ bool build_page_skip_plan_for_leaf( int64_t row_group_rows, ParquetPageSkipPlan* page_skip_plan) { DORIS_CHECK(page_skip_plan != nullptr); *page_skip_plan = ParquetPageSkipPlan {}; - // OffsetIndex first_row_index is row-based only for non-repeated leaves. LIST/MAP/repeated - // leaves need repetition-level-aware range mapping and are intentionally left out for now. if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE || column_schema.descriptor == nullptr || column_schema.leaf_column_id < 0 || column_schema.descriptor->max_repetition_level() != 0) { @@ -1212,7 +1335,8 @@ Status select_row_group_ranges_by_page_index( const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, int row_group_idx, int64_t row_group_rows, std::vector<RowRange>* selected_ranges, std::map<int, ParquetPageSkipPlan>* page_skip_plans, - ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone) { + ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone, + const RuntimeState* runtime_state) { int64_t page_index_filter_time_sink = 0; SCOPED_RAW_TIMER(pruning_stats == nullptr ? &page_index_filter_time_sink : &pruning_stats->page_index_filter_time); @@ -1225,7 +1349,7 @@ Status select_row_group_ranges_by_page_index( return Status::OK(); } selected_ranges->push_back(RowRange {0, row_group_rows}); - if (!config::enable_parquet_page_index || request.column_predicate_filters.empty() || + if (!config::enable_parquet_page_index || !has_expr_zonemap_filter(request, runtime_state) || file_reader == nullptr) { return Status::OK(); } @@ -1255,10 +1379,19 @@ Status select_row_group_ranges_by_page_index( return Status::OK(); } - for (const auto& column_filter : request.column_predicate_filters) { + std::map<int, VExprContextSPtrs> conjuncts_by_slot; + for (const auto& conjunct : request.conjuncts) { + const auto slot_index = expr_zonemap::single_slot_zonemap_index(conjunct); + if (slot_index >= 0) { + conjuncts_by_slot[slot_index].push_back(conjunct); + } + } + + for (const auto& [slot_index, conjuncts] : conjuncts_by_slot) { std::vector<RowRange> filter_ranges; - if (!select_ranges_for_filter(row_group_index_reader, file_schema, column_filter, - row_group_rows, &filter_ranges, timezone)) { + if (!select_ranges_for_expr_zonemap(row_group_index_reader, file_schema, request, + slot_index, conjuncts, row_group_rows, &filter_ranges, + pruning_stats, timezone)) { continue; } *selected_ranges = intersect_ranges(*selected_ranges, filter_ranges); diff --git a/be/src/format_v2/parquet/parquet_statistics.h b/be/src/format_v2/parquet/parquet_statistics.h index 3d4b9d35791..d9c9e45b171 100644 --- a/be/src/format_v2/parquet/parquet_statistics.h +++ b/be/src/format_v2/parquet/parquet_statistics.h @@ -38,6 +38,7 @@ class time_zone; namespace doris { class ColumnPredicate; +class RuntimeState; } // namespace doris namespace doris::format::parquet { @@ -50,7 +51,7 @@ struct ParquetColumnSchema; 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 - int64_t filtered_row_groups_by_statistics = 0; // row groups pruned by min/max statistics + int64_t filtered_row_groups_by_statistics = 0; // row groups pruned by ZoneMap statistics int64_t filtered_row_groups_by_dictionary = 0; // row groups pruned by dictionary int64_t filtered_row_groups_by_bloom_filter = 0; // row groups pruned by bloom filter int64_t filtered_row_groups_by_page_index = 0; // row groups fully pruned by page index @@ -62,6 +63,9 @@ struct ParquetPruningStats { int64_t row_group_filter_time = 0; // row-group pruning time (ns) int64_t page_index_filter_time = 0; // page-index pruning time (ns) int64_t read_page_index_time = 0; // page-index read time (ns) + int64_t expr_zonemap_unusable_evals = 0; // VExpr ZoneMap unusable evaluations + int64_t in_zonemap_point_check_count = 0; // VExpr IN ZoneMap point checks + int64_t in_zonemap_range_only_count = 0; // VExpr IN ZoneMap range-only checks }; struct ParquetColumnStatistics { @@ -77,8 +81,9 @@ struct ParquetColumnStatistics { // ============================================================================ // ============================================================================ -// statistics(TransformColumnStatistics + check_statistics) -// -> dictionary(read_dictionary_words + predicate::evaluate_and) +// VExpr ZoneMap(TransformColumnStatistics + evaluate_zonemap_filter) +// -> page-index ZoneMap(evaluate_zonemap_filter) +// dictionary(read_dictionary_words + predicate::evaluate_and) // -> bloom filter(bloom_filter_prune_reason) // ============================================================================ struct ParquetStatisticsUtils { @@ -97,13 +102,15 @@ Status select_row_groups_by_statistics( const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, const std::vector<int>* candidate_row_groups, std::vector<int>* selected_row_groups, bool enable_bloom_filter, - ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone = nullptr); + ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone = nullptr, + const RuntimeState* runtime_state = nullptr); Status select_row_group_ranges_by_page_index( ::parquet::ParquetFileReader* file_reader, const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, const format::FileScanRequest& request, int row_group_idx, int64_t row_group_rows, std::vector<RowRange>* selected_ranges, std::map<int, ParquetPageSkipPlan>* page_skip_plans, - ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone = nullptr); + ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone = nullptr, + const RuntimeState* runtime_state = nullptr); } // namespace doris::format::parquet diff --git a/be/test/format_v2/parquet/parquet_reader_test.cpp b/be/test/format_v2/parquet/parquet_reader_test.cpp index 7fbfacd5d5d..eb984d528f7 100644 --- a/be/test/format_v2/parquet/parquet_reader_test.cpp +++ b/be/test/format_v2/parquet/parquet_reader_test.cpp @@ -30,6 +30,7 @@ #include <memory> #include <numeric> #include <optional> +#include <set> #include <string> #include <utility> #include <vector> @@ -60,6 +61,8 @@ #include "gen_cpp/Types_types.h" #include "io/io_common.h" #include "runtime/runtime_state.h" +#include "storage/index/zone_map/zonemap_eval_context.h" +#include "storage/index/zone_map/zonemap_filter_result.h" #include "storage/predicate/predicate_creator.h" #include "storage/segment/condition_cache.h" #include "storage/utils.h" @@ -114,6 +117,25 @@ public: const std::string& expr_name() const override { return _expr_name; } + bool can_evaluate_zonemap_filter() const override { return true; } + + void collect_slot_column_ids(std::set<int>& column_ids) const override { + column_ids.insert(_column_id); + } + + ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) const override { + auto zone_map = ctx.zone_map(_column_id); + if (zone_map == nullptr) { + return unsupported_zonemap_filter(ctx); + } + if (!zone_map->has_not_null) { + return ZoneMapFilterResult::kNoMatch; + } + const auto literal = Field::create_field<TYPE_INT>(_value); + return zone_map->max_value <= literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + private: const int _column_id; const int32_t _value; @@ -1748,12 +1770,8 @@ TEST_F(NewParquetReaderTest, PlannerNarrowsRowRangesByPageIndex) { ASSERT_EQ(file_schema.size(), 1); format::FileScanRequest request; - format::FileColumnPredicateFilter column_filter; - column_filter.file_column_id = format::LocalColumnId(0); - auto id_type = std::make_shared<DataTypeInt32>(); - column_filter.predicates.push_back(create_comparison_predicate<PredicateType::GT>( - 0, "id", id_type, Field::create_field<TYPE_INT>(63), false)); - request.column_predicate_filters.push_back(std::move(column_filter)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_greater_than_conjunct(0, 63)); format::parquet::RowGroupScanPlan plan; format::parquet::ParquetScanRange scan_range; @@ -1808,12 +1826,8 @@ TEST_F(NewParquetReaderTest, NestedStructPredicateDoesNotNarrowRowRangesByPageIn ASSERT_EQ(file_schema[0]->children[0]->name, "id"); format::FileScanRequest request; - format::FileColumnPredicateFilter column_filter; - column_filter.file_column_id = format::LocalColumnId(0); - auto id_type = std::make_shared<DataTypeInt32>(); - column_filter.predicates.push_back(create_comparison_predicate<PredicateType::GT>( - 0, "id", id_type, Field::create_field<TYPE_INT>(63), false)); - request.column_predicate_filters.push_back(std::move(column_filter)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_greater_than_conjunct(0, 63)); format::parquet::RowGroupScanPlan plan; format::parquet::ParquetScanRange scan_range; diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp b/be/test/format_v2/parquet/parquet_scan_test.cpp index 3b381c3158f..e3efd6e1b93 100644 --- a/be/test/format_v2/parquet/parquet_scan_test.cpp +++ b/be/test/format_v2/parquet/parquet_scan_test.cpp @@ -28,6 +28,7 @@ #include <memory> #include <numeric> #include <optional> +#include <set> #include <string> #include <utility> #include <vector> @@ -41,6 +42,8 @@ #include "core/data_type/data_type_number.h" #include "core/data_type/data_type_string.h" #include "core/field.h" +#include "exprs/vexpr.h" +#include "exprs/vexpr_context.h" #include "format_v2/file_reader.h" #include "format_v2/parquet/parquet_column_schema.h" #include "format_v2/parquet/parquet_reader.h" @@ -48,6 +51,8 @@ #include "gen_cpp/Types_types.h" #include "io/io_common.h" #include "runtime/runtime_state.h" +#include "storage/index/zone_map/zonemap_eval_context.h" +#include "storage/index/zone_map/zonemap_filter_result.h" #include "storage/predicate/predicate_creator.h" #include "storage/utils.h" @@ -72,6 +77,57 @@ const ColumnString& string_data_column(const IColumn& column) { return assert_cast<const ColumnString&>(column); } +class Int32ZoneMapExpr final : public VExpr { +public: + enum class Op { GE, GT, LT }; + + Int32ZoneMapExpr(int column_id, Op op, int32_t value) + : VExpr(std::make_shared<DataTypeUInt8>(), false), + _column_id(column_id), + _op(op), + _value(value) {} + + const std::string& expr_name() const override { return _expr_name; } + + bool can_evaluate_zonemap_filter() const override { return true; } + + void collect_slot_column_ids(std::set<int>& column_ids) const override { + column_ids.insert(_column_id); + } + + ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) const override { + auto zone_map = ctx.zone_map(_column_id); + if (zone_map == nullptr) { + return unsupported_zonemap_filter(ctx); + } + if (!zone_map->has_not_null) { + return ZoneMapFilterResult::kNoMatch; + } + const auto literal = Field::create_field<TYPE_INT>(_value); + if (_op == Op::GE) { + return zone_map->max_value < literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + if (_op == Op::GT) { + return zone_map->max_value <= literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + return zone_map->min_value >= literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + +private: + int _column_id; + Op _op; + int32_t _value; + const std::string _expr_name = "Int32ZoneMapExpr"; +}; + +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)); +} + std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) { std::shared_ptr<arrow::Array> array; EXPECT_TRUE(builder->Finish(&array).ok()); @@ -330,8 +386,8 @@ TEST_F(ParquetScanTest, PlanRowGroupsAppliesScanRangeBeforeStatistics) { auto file_schema = build_file_schema(*parquet_file_reader); format::FileScanRequest request; - request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GE, 5)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GE, 5)); const auto [range_start_offset, range_size] = row_group_mid_range(_file_path, 1); format::parquet::ParquetScanRange scan_range; @@ -358,8 +414,8 @@ TEST_F(ParquetScanTest, PlanRowGroupsPreservesFirstFileRowAcrossPrunedRowGroups) auto file_schema = build_file_schema(*parquet_file_reader); format::FileScanRequest request; - request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GE, 5)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GE, 5)); format::parquet::RowGroupScanPlan plan; format::parquet::ParquetScanRange scan_range; @@ -413,8 +469,9 @@ TEST_F(ParquetScanTest, PageIndexIntersectsMultipleFiltersAndBuildsSkipPlan) { auto file_schema = build_file_schema(*parquet_file_reader); format::FileScanRequest single_filter_request; - single_filter_request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GE, 32)); + single_filter_request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + single_filter_request.conjuncts.push_back( + create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GE, 32)); format::parquet::RowGroupScanPlan single_filter_plan; format::parquet::ParquetScanRange scan_range; ASSERT_TRUE(format::parquet::plan_parquet_row_groups( @@ -426,10 +483,12 @@ TEST_F(ParquetScanTest, PageIndexIntersectsMultipleFiltersAndBuildsSkipPlan) { count_range_rows(single_filter_plan.row_groups[0].selected_ranges); format::FileScanRequest intersect_request; - intersect_request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GE, 32)); - intersect_request.column_predicate_filters.push_back( - int32_filter(1, "score", file_schema[1]->type, PredicateType::LT, 96)); + intersect_request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + intersect_request.local_positions.emplace(format::LocalColumnId(1), format::LocalIndex(1)); + intersect_request.conjuncts.push_back( + create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GE, 32)); + intersect_request.conjuncts.push_back( + create_int32_zonemap_conjunct(1, Int32ZoneMapExpr::Op::LT, 96)); format::parquet::RowGroupScanPlan intersect_plan; ASSERT_TRUE(format::parquet::plan_parquet_row_groups( *parquet_file_reader->metadata(), parquet_file_reader.get(), file_schema, @@ -463,10 +522,9 @@ TEST_F(ParquetScanTest, PageIndexCanFullyFilterRowGroupAfterRangeIntersection) { auto file_schema = build_file_schema(*parquet_file_reader); format::FileScanRequest request; - request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GE, 32)); - request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::LT, 32)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GE, 32)); + request.conjuncts.push_back(create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::LT, 32)); format::parquet::RowGroupScanPlan plan; format::parquet::ParquetScanRange scan_range; @@ -488,8 +546,8 @@ TEST_F(ParquetScanTest, PageIndexFullRangeWhenDisabledOrUnavailable) { auto file_schema = build_file_schema(*parquet_file_reader); format::FileScanRequest request; - request.column_predicate_filters.push_back( - int32_filter(0, "id", file_schema[0]->type, PredicateType::GT, 63)); + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GT, 63)); const bool old_enable_page_index = config::enable_parquet_page_index; config::enable_parquet_page_index = false; @@ -511,8 +569,9 @@ TEST_F(ParquetScanTest, PageIndexFullRangeWhenDisabledOrUnavailable) { auto no_index_reader = ::parquet::ParquetFileReader::OpenFile(_file_path, false); auto no_index_schema = build_file_schema(*no_index_reader); format::FileScanRequest no_index_request; - no_index_request.column_predicate_filters.push_back( - int32_filter(0, "id", no_index_schema[0]->type, PredicateType::GT, 3)); + no_index_request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + no_index_request.conjuncts.push_back( + create_int32_zonemap_conjunct(0, Int32ZoneMapExpr::Op::GT, 3)); selected_ranges.clear(); page_skip_plans.clear(); pruning_stats = {}; diff --git a/be/test/format_v2/parquet/parquet_statistics_test.cpp b/be/test/format_v2/parquet/parquet_statistics_test.cpp index ae9ee4bf1e3..615fb479b2c 100644 --- a/be/test/format_v2/parquet/parquet_statistics_test.cpp +++ b/be/test/format_v2/parquet/parquet_statistics_test.cpp @@ -28,14 +28,19 @@ #include <memory> #include <numeric> #include <optional> +#include <set> #include <string> #include <vector> #include "core/data_type/data_type_number.h" #include "core/data_type/data_type_string.h" #include "core/field.h" +#include "exprs/vexpr.h" +#include "exprs/vexpr_context.h" #include "format_v2/file_reader.h" #include "format_v2/parquet/parquet_column_schema.h" +#include "storage/index/zone_map/zonemap_eval_context.h" +#include "storage/index/zone_map/zonemap_filter_result.h" #include "storage/predicate/accept_null_predicate.h" #include "storage/predicate/null_predicate.h" #include "storage/predicate/predicate_creator.h" @@ -146,6 +151,63 @@ format::FileScanRequest request_with_filter(format::FileColumnPredicateFilter fi return request; } +class Int32ZoneMapExpr final : public VExpr { +public: + enum class Op { GE, GT, IS_NULL, IS_NOT_NULL }; + + Int32ZoneMapExpr(int column_id, Op op, int32_t value = 0) + : VExpr(std::make_shared<DataTypeUInt8>(), false), + _column_id(column_id), + _op(op), + _value(value) {} + + const std::string& expr_name() const override { return _expr_name; } + + bool can_evaluate_zonemap_filter() const override { return true; } + + void collect_slot_column_ids(std::set<int>& column_ids) const override { + column_ids.insert(_column_id); + } + + ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) const override { + auto zone_map = ctx.zone_map(_column_id); + if (zone_map == nullptr) { + return unsupported_zonemap_filter(ctx); + } + if (_op == Op::IS_NULL) { + return zone_map->has_null ? ZoneMapFilterResult::kMayMatch + : ZoneMapFilterResult::kNoMatch; + } + if (_op == Op::IS_NOT_NULL) { + return zone_map->has_not_null ? ZoneMapFilterResult::kMayMatch + : ZoneMapFilterResult::kNoMatch; + } + if (!zone_map->has_not_null) { + return ZoneMapFilterResult::kNoMatch; + } + const auto literal = Field::create_field<TYPE_INT>(_value); + if (_op == Op::GE) { + return zone_map->max_value < literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + return zone_map->max_value <= literal ? ZoneMapFilterResult::kNoMatch + : ZoneMapFilterResult::kMayMatch; + } + +private: + int _column_id; + Op _op; + int32_t _value; + const std::string _expr_name = "Int32ZoneMapExpr"; +}; + +format::FileScanRequest request_with_zonemap_conjunct(std::shared_ptr<VExpr> expr) { + format::FileScanRequest request; + request.local_positions.emplace(format::LocalColumnId(0), format::LocalIndex(0)); + request.conjuncts.push_back(VExprContext::create_shared(std::move(expr))); + return request; +} + ::parquet::BlockSplitBloomFilter bloom_filter_for_int32_values(const std::vector<int32_t>& values) { ::parquet::BlockSplitBloomFilter bloom_filter; bloom_filter.Init(::parquet::BlockSplitBloomFilter::kMinimumBloomFilterBytes); @@ -223,45 +285,38 @@ TEST(ParquetStatisticsTransformTest, HandlesMissingStatisticsAndAllNullChunks) { EXPECT_FALSE(all_null_stats.has_min_max); } -TEST(ParquetStatisticsPruningTest, StatisticsPredicatesAndNullPredicatesPruneRowGroups) { +TEST(ParquetStatisticsPruningTest, ExprZonemapPredicatesAndNullPredicatesPruneRowGroups) { auto table = arrow::Table::Make(arrow::schema({arrow::field("i", arrow::int32(), true)}), {int32_array({std::nullopt, std::nullopt, 3, 4, 5, 6})}); auto reader = make_reader(table, 2, false, true); auto schema = build_file_schema(*reader); - format::FileColumnPredicateFilter ge_filter; - ge_filter.file_column_id = format::LocalColumnId(0); - ge_filter.predicates.push_back(create_comparison_predicate<PredicateType::GE>( - 0, "i", schema[0]->type, Field::create_field<TYPE_INT>(5), false)); std::vector<int> selected; format::parquet::ParquetPruningStats pruning_stats; ASSERT_TRUE(format::parquet::select_row_groups_by_statistics( - *reader->metadata(), reader.get(), schema, request_with_filter(ge_filter), + *reader->metadata(), reader.get(), schema, + request_with_zonemap_conjunct( + std::make_shared<Int32ZoneMapExpr>(0, Int32ZoneMapExpr::Op::GE, 5)), nullptr, &selected, false, &pruning_stats) .ok()); EXPECT_EQ(selected, std::vector<int>({2})); EXPECT_EQ(pruning_stats.filtered_row_groups_by_statistics, 2); - format::FileColumnPredicateFilter is_not_null_filter; - is_not_null_filter.file_column_id = format::LocalColumnId(0); - is_not_null_filter.predicates.push_back( - std::make_shared<NullPredicate>(0, "i", false, TYPE_INT)); selected.clear(); ASSERT_TRUE(format::parquet::select_row_groups_by_statistics( *reader->metadata(), reader.get(), schema, - request_with_filter(is_not_null_filter), nullptr, &selected, false, - &pruning_stats) + request_with_zonemap_conjunct(std::make_shared<Int32ZoneMapExpr>( + 0, Int32ZoneMapExpr::Op::IS_NOT_NULL)), + nullptr, &selected, false, &pruning_stats) .ok()); EXPECT_EQ(selected, std::vector<int>({1, 2})); - format::FileColumnPredicateFilter is_null_filter; - is_null_filter.file_column_id = format::LocalColumnId(0); - is_null_filter.predicates.push_back(std::make_shared<NullPredicate>(0, "i", true, TYPE_INT)); selected.clear(); ASSERT_TRUE(format::parquet::select_row_groups_by_statistics( *reader->metadata(), reader.get(), schema, - request_with_filter(is_null_filter), nullptr, &selected, false, - &pruning_stats) + request_with_zonemap_conjunct(std::make_shared<Int32ZoneMapExpr>( + 0, Int32ZoneMapExpr::Op::IS_NULL)), + nullptr, &selected, false, &pruning_stats) .ok()); EXPECT_EQ(selected, std::vector<int>({0})); } @@ -313,26 +368,26 @@ TEST(ParquetStatisticsPruningTest, DictionaryPruningHandlesExcludeIncludeAndUnsu EXPECT_EQ(pruning_stats.filtered_row_groups_by_dictionary, 0); } -TEST(ParquetStatisticsPruningTest, StatisticsRunsBeforeDictionaryAndMissingBloomKeepsRows) { +TEST(ParquetStatisticsPruningTest, ColumnPredicatesUseDictionaryAndMissingBloomKeepsRows) { auto table = arrow::Table::Make(arrow::schema({arrow::field("s", arrow::utf8(), false)}), {string_array({"alpha", "beta", "gamma", "omega"})}); auto reader = make_reader(table, 2, true, true); auto schema = build_file_schema(*reader); - format::FileColumnPredicateFilter beyond_max_filter; - beyond_max_filter.file_column_id = format::LocalColumnId(0); - beyond_max_filter.predicates.push_back(create_comparison_predicate<PredicateType::GT>( - 0, "s", schema[0]->type, Field::create_field<TYPE_STRING>("zzzz"), false)); std::vector<int> selected; format::parquet::ParquetPruningStats pruning_stats; + format::FileColumnPredicateFilter absent_filter; + absent_filter.file_column_id = format::LocalColumnId(0); + absent_filter.predicates.push_back(create_comparison_predicate<PredicateType::EQ>( + 0, "s", schema[0]->type, Field::create_field<TYPE_STRING>("absent"), false)); ASSERT_TRUE(format::parquet::select_row_groups_by_statistics( *reader->metadata(), reader.get(), schema, - request_with_filter(beyond_max_filter), nullptr, &selected, true, + request_with_filter(absent_filter), nullptr, &selected, true, &pruning_stats) .ok()); EXPECT_TRUE(selected.empty()); - EXPECT_EQ(pruning_stats.filtered_row_groups_by_statistics, 2); - EXPECT_EQ(pruning_stats.filtered_row_groups_by_dictionary, 0); + EXPECT_EQ(pruning_stats.filtered_row_groups_by_statistics, 0); + EXPECT_EQ(pruning_stats.filtered_row_groups_by_dictionary, 2); EXPECT_EQ(pruning_stats.filtered_row_groups_by_bloom_filter, 0); auto no_stats_reader = make_reader(table, 2, false, false); diff --git a/be/test/format_v2/table/iceberg_reader_test.cpp b/be/test/format_v2/table/iceberg_reader_test.cpp index 84fe09bc0c5..a52f4f22eb3 100644 --- a/be/test/format_v2/table/iceberg_reader_test.cpp +++ b/be/test/format_v2/table/iceberg_reader_test.cpp @@ -1148,8 +1148,8 @@ TEST(IcebergV2ReaderTest, IcebergVirtualColumnsKeepRowLineageAfterRowGroupPredic std::filesystem::create_directories(test_dir); const auto file_path = (test_dir / "split.parquet").string(); - // ColumnPredicate is used for row-group/statistics pruning. Keep one row per row group so - // id > 2 prunes the first two row groups and leaves only the third file-local row. + // Keep one row per row group so the VExpr ZoneMap path can prune the first two row groups and + // leave only the third file-local row. write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"}, 1); std::vector<ColumnDefinition> projected_columns; @@ -1168,7 +1168,8 @@ TEST(IcebergV2ReaderTest, IcebergVirtualColumnsKeepRowLineageAfterRowGroupPredic ASSERT_TRUE(reader.init({ .projected_columns = projected_columns, .column_predicates = std::move(column_predicates), - .conjuncts = {}, + .conjuncts = {prepared_conjunct( + &state, table_int32_greater_than_expr(2, 2, 2))}, .format = FileFormat::PARQUET, .scan_params = nullptr, .io_ctx = nullptr, diff --git a/be/test/format_v2/table_reader_test.cpp b/be/test/format_v2/table_reader_test.cpp index a8659667688..57762c24202 100644 --- a/be/test/format_v2/table_reader_test.cpp +++ b/be/test/format_v2/table_reader_test.cpp @@ -2862,8 +2862,8 @@ TEST(TableReaderTest, OpenReaderBuildsColumnPredicateFilters) { std::filesystem::create_directories(test_dir); const auto file_path = (test_dir / "split.parquet").string(); - // ColumnPredicate is only used for row-group/statistics pruning. Keep one row per row - // group so the predicate can prune the first two row groups and leave only id = 3. + // Keep one row per row group so the VExpr ZoneMap path can prune the first two row groups and + // leave only id = 3. write_int_pair_parquet_file(file_path, {1, 2, 3}, {1, 5, 8}, {"one", "two", "three"}, 1); std::vector<ColumnDefinition> projected_columns; @@ -2882,7 +2882,8 @@ TEST(TableReaderTest, OpenReaderBuildsColumnPredicateFilters) { ASSERT_TRUE(reader.init({ .projected_columns = projected_columns, .column_predicates = std::move(column_predicates), - .conjuncts = {}, + .conjuncts = {prepared_conjunct( + &state, table_int32_greater_than_expr(1, 1, 2))}, .format = FileFormat::PARQUET, .scan_params = nullptr, .io_ctx = nullptr, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
