-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[improvement](be) Prune nested Parquet leaves with Bloom filters #66423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
19d48a3
866a690
3699963
096a8af
4285e13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,10 @@ | |
| #include "exprs/expr_zonemap_filter.h" | ||
|
|
||
| #include <algorithm> | ||
| #include <cmath> | ||
| #include <limits> | ||
| #include <set> | ||
| #include <type_traits> | ||
| #include <utility> | ||
|
|
||
| #include "common/check.h" | ||
|
|
@@ -49,6 +52,33 @@ std::optional<std::pair<Field, DataTypePtr>> field_from_literal_expr(const VExpr | |
| return std::make_pair(std::move(field), literal->get_data_type()); | ||
| } | ||
|
|
||
| std::optional<int32_t> struct_field_ordinal(const Field& field) { | ||
| int64_t ordinal = -1; | ||
| switch (field.get_type()) { | ||
| case TYPE_BOOLEAN: | ||
| ordinal = field.get<TYPE_BOOLEAN>(); | ||
| break; | ||
| case TYPE_TINYINT: | ||
| ordinal = field.get<TYPE_TINYINT>(); | ||
| break; | ||
| case TYPE_SMALLINT: | ||
| ordinal = field.get<TYPE_SMALLINT>(); | ||
| break; | ||
| case TYPE_INT: | ||
| ordinal = field.get<TYPE_INT>(); | ||
| break; | ||
| case TYPE_BIGINT: | ||
| ordinal = field.get<TYPE_BIGINT>(); | ||
| break; | ||
| default: | ||
| return std::nullopt; | ||
| } | ||
| if (ordinal <= 0 || ordinal > std::numeric_limits<int32_t>::max()) { | ||
| return std::nullopt; | ||
| } | ||
| return static_cast<int32_t>(ordinal - 1); | ||
| } | ||
|
|
||
| bool value_in_range(const Field& value, const Field& min_value, const Field& max_value) { | ||
| return value >= min_value && value <= max_value; | ||
| } | ||
|
|
@@ -60,6 +90,29 @@ bool dictionary_contains(const DictionaryEvalContext::SlotDictionary& dictionary | |
| }); | ||
| } | ||
|
|
||
| bool bloom_filter_probes_equal(const BloomFilterProbe& lhs, const BloomFilterProbe& rhs) { | ||
| return lhs.slot_index == rhs.slot_index && lhs.path == rhs.path && | ||
| data_types_compatible(lhs.value_type, rhs.value_type); | ||
| } | ||
|
|
||
| template <typename T> | ||
| bool floating_point_bloom_filter_may_contain(const segment_v2::BloomFilter& bloom_filter, T value) { | ||
| static_assert(std::is_floating_point_v<T>); | ||
| // Doris equality collapses NaN payloads and signed zeros, while Parquet Bloom hashes physical | ||
| // bytes. A negative probe is safe only after covering the entire Doris-equivalent class. | ||
| if (std::isnan(value)) { | ||
| return true; | ||
| } | ||
| const auto test_value = [&](T candidate) { | ||
| return bloom_filter.test_bytes(reinterpret_cast<const char*>(&candidate), | ||
| sizeof(candidate)); | ||
| }; | ||
| if (test_value(value)) { | ||
| return true; | ||
| } | ||
| return value == T {0} && test_value(-value); | ||
| } | ||
|
|
||
| bool bloom_filter_may_contain(const BloomFilterEvalContext::SlotBloomFilter& slot_filter, | ||
| const Field& value) { | ||
| DORIS_CHECK(slot_filter.data_type != nullptr); | ||
|
|
@@ -84,13 +137,11 @@ bool bloom_filter_may_contain(const BloomFilterEvalContext::SlotBloomFilter& slo | |
| } | ||
| case TYPE_FLOAT: { | ||
| const float typed_value = value.get<TYPE_FLOAT>(); | ||
| return slot_filter.bloom_filter->test_bytes(reinterpret_cast<const char*>(&typed_value), | ||
| sizeof(typed_value)); | ||
| return floating_point_bloom_filter_may_contain(*slot_filter.bloom_filter, typed_value); | ||
| } | ||
| case TYPE_DOUBLE: { | ||
| const double typed_value = value.get<TYPE_DOUBLE>(); | ||
| return slot_filter.bloom_filter->test_bytes(reinterpret_cast<const char*>(&typed_value), | ||
| sizeof(typed_value)); | ||
| return floating_point_bloom_filter_may_contain(*slot_filter.bloom_filter, typed_value); | ||
| } | ||
| case TYPE_CHAR: | ||
| case TYPE_VARCHAR: | ||
|
|
@@ -213,6 +264,123 @@ std::optional<SlotLiteral> extract_slot_and_literal(const VExprSPtrs& args) { | |
| return std::nullopt; | ||
| } | ||
|
|
||
| std::optional<BloomFilterProbe> extract_bloom_filter_probe(const VExprSPtr& expr) { | ||
| if (expr == nullptr || expr->data_type() == nullptr) { | ||
| return std::nullopt; | ||
| } | ||
| if (auto slot = std::dynamic_pointer_cast<VSlotRef>(expr); slot) { | ||
| return BloomFilterProbe { | ||
| .slot_index = slot->column_id(), .value_type = slot->data_type(), .path = {}}; | ||
| } | ||
| if ((expr->fn().name.function_name != "element_at" && | ||
| expr->fn().name.function_name != "struct_element") || | ||
| expr->get_num_children() != 2) { | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| auto probe = extract_bloom_filter_probe(expr->get_child(0)); | ||
| auto selector = field_from_literal_expr(expr->get_child(1)); | ||
| if (!probe.has_value() || !selector.has_value() || selector->first.is_null()) { | ||
| return std::nullopt; | ||
| } | ||
| const auto parent_type = remove_nullable(expr->get_child(0)->data_type()); | ||
| if (parent_type == nullptr) { | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| BloomFilterPathElement path_element; | ||
| switch (parent_type->get_primitive_type()) { | ||
| case TYPE_STRUCT: { | ||
| path_element.kind = BloomFilterPathKind::STRUCT_FIELD; | ||
| const auto selector_type = remove_nullable(selector->second); | ||
| if (selector_type == nullptr) { | ||
| return std::nullopt; | ||
| } | ||
| if (is_string_type(selector_type->get_primitive_type())) { | ||
| path_element.field_name = selector->first.get<TYPE_STRING>(); | ||
| } else { | ||
| auto ordinal = struct_field_ordinal(selector->first); | ||
| if (!ordinal.has_value()) { | ||
| return std::nullopt; | ||
| } | ||
| path_element.field_ordinal = *ordinal; | ||
| } | ||
| break; | ||
| } | ||
| case TYPE_ARRAY: | ||
| // Array element positions share one repeated Parquet leaf; membership in that leaf is a | ||
| // necessary condition for any element_at(array, constant) equality to match. | ||
| path_element.kind = BloomFilterPathKind::LIST_ELEMENT; | ||
| break; | ||
| default: | ||
| return std::nullopt; | ||
| } | ||
| probe->value_type = expr->data_type(); | ||
| probe->path.push_back(std::move(path_element)); | ||
| return probe; | ||
| } | ||
|
|
||
| bool collect_unique_bloom_filter_probe(const VExprSPtr& expr, | ||
| std::optional<BloomFilterProbe>* result) { | ||
| DORIS_CHECK(result != nullptr); | ||
| if (auto probe = extract_bloom_filter_probe(expr); probe.has_value()) { | ||
| if (result->has_value() && !bloom_filter_probes_equal(**result, *probe)) { | ||
| return false; | ||
| } | ||
| *result = std::move(probe); | ||
| return true; | ||
| } | ||
| if (expr == nullptr) { | ||
| return true; | ||
| } | ||
| for (uint16_t child_idx = 0; child_idx < expr->get_num_children(); ++child_idx) { | ||
| const auto& child = expr->get_child(child_idx); | ||
| if (child == nullptr || child->is_literal()) { | ||
| continue; | ||
| } | ||
| // Every Bloom-capable branch must bind to the same leaf; a conflicting subtree cannot be | ||
| // treated like a branch without a probe because the compound evaluator would use it. | ||
| if (!collect_unique_bloom_filter_probe(child, result)) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| std::optional<BloomFilterProbe> extract_bloom_filter_predicate_probe(const VExprSPtr& expr) { | ||
| std::optional<BloomFilterProbe> result; | ||
| if (!collect_unique_bloom_filter_probe(expr, &result)) { | ||
| return std::nullopt; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| std::optional<SlotLiteral> extract_bloom_filter_slot_and_literal(const VExprSPtrs& args) { | ||
| if (args.size() != 2) { | ||
| return std::nullopt; | ||
| } | ||
| for (size_t probe_idx = 0; probe_idx < args.size(); ++probe_idx) { | ||
| auto probe = extract_bloom_filter_probe(args[probe_idx]); | ||
| auto literal = field_from_literal_expr(args[1 - probe_idx]); | ||
| if (!probe.has_value() || !literal.has_value()) { | ||
| continue; | ||
| } | ||
| auto [literal_value, literal_type] = std::move(*literal); | ||
| return SlotLiteral {.slot_index = probe->slot_index, | ||
| .slot_type = probe->value_type, | ||
| .literal = std::move(literal_value), | ||
| .literal_type = std::move(literal_type), | ||
| .literal_on_left = probe_idx == 1}; | ||
| } | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| bool can_evaluate_bloom_filter_equality(const VExprSPtrs& args) { | ||
| auto slot_literal = extract_bloom_filter_slot_and_literal(args); | ||
| return slot_literal.has_value() && !slot_literal->literal.is_null() && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Preserve FLOAT/DOUBLE equality classes when probing Parquet Blooms This capability now admits nested FLOAT/DOUBLE equality (and is reused by null-safe equality), but the native adapter hashes the literal's raw IEEE bytes. Doris considers |
||
| data_types_compatible(slot_literal->slot_type, slot_literal->literal_type); | ||
| } | ||
|
|
||
| bool range_stats_usable_for_zonemap(const segment_v2::ZoneMap& zone_map, | ||
| const DataTypePtr& data_type) { | ||
| if (zone_map.pass_all || zone_map.has_nan || zone_map.has_positive_inf || | ||
|
|
@@ -374,14 +542,14 @@ ZoneMapFilterResult eval_in_bloom_filter(const BloomFilterEvalContext& ctx, | |
| if (is_not_in) { | ||
| return ZoneMapFilterResult::kUnsupported; | ||
| } | ||
| auto slot = std::dynamic_pointer_cast<VSlotRef>(slot_expr); | ||
| DORIS_CHECK(slot != nullptr); | ||
| auto slot_filter = ctx.slot(slot->column_id()); | ||
| auto probe = extract_bloom_filter_probe(slot_expr); | ||
| DORIS_CHECK(probe.has_value()); | ||
| auto slot_filter = ctx.slot(probe->slot_index); | ||
| if (slot_filter == nullptr || slot_filter->data_type == nullptr || | ||
| slot_filter->bloom_filter == nullptr) { | ||
| return ZoneMapFilterResult::kUnsupported; | ||
| } | ||
| DORIS_CHECK(data_types_compatible(slot_filter->data_type, slot->data_type())); | ||
| DORIS_CHECK(data_types_compatible(slot_filter->data_type, probe->value_type)); | ||
| if (values.empty()) { | ||
| return ZoneMapFilterResult::kNoMatch; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -671,11 +671,24 @@ bool VectorizedFnCall::is_deterministic() const { | |
| } | ||
|
|
||
| bool VectorizedFnCall::is_safe_to_execute_on_selected_rows() const { | ||
| static const std::set<std::string> TOTAL_PREDICATE_FUNCTIONS = { | ||
| "eq", "ne", "lt", "le", "gt", "ge", "in", "not_in", "is_null_pred", "is_not_null_pred"}; | ||
| static const std::set<std::string> TOTAL_PREDICATE_FUNCTIONS = {"eq", | ||
| "eq_for_null", | ||
| "ne", | ||
| "lt", | ||
| "le", | ||
| "gt", | ||
| "ge", | ||
| "in", | ||
| "not_in", | ||
| "is_null_pred", | ||
| "is_not_null_pred", | ||
| "element_at", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Fence constant pruning after a rejected accessor Adding |
||
| "struct_element"}; | ||
| // Selected-row execution may hide data-dependent errors in rows rejected by an earlier | ||
| // predicate. Keep function calls unsafe by default and opt in only operations that are total | ||
| // for their input domain; child checks then reject expressions such as gt(mod(x, -1), 0). | ||
| // for their input domain. Accessors return NULL for absent elements, so admitting them keeps | ||
| // nested metadata predicates reachable without crossing an error-producing child such as | ||
| // gt(mod(x, -1), 0). | ||
| return TOTAL_PREDICATE_FUNCTIONS.contains(_function_name) && | ||
| VExpr::is_safe_to_execute_on_selected_rows(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,11 +159,17 @@ Status VInPredicate::_materialize_for_zonemap_filter(VExprContext* context) { | |
| _seg_filter_contains_null = false; | ||
| _zonemap_materialized = false; | ||
| _direct_filter_set.reset(); | ||
| if (_children.size() < 2 || !_children[0]->is_slot_ref()) { | ||
| if (_children.size() < 2) { | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| const auto data_type = remove_nullable(_children[0]->data_type()); | ||
| auto bloom_probe = expr_zonemap::extract_bloom_filter_probe(_children[0]); | ||
| if (!bloom_probe.has_value()) { | ||
| return Status::OK(); | ||
| } | ||
| // Materialization is shared by all pruning paths. Their capability checks keep ZoneMap, | ||
| // dictionary, and raw evaluation direct-slot-only while Bloom may consume a nested leaf. | ||
| const auto data_type = remove_nullable(bloom_probe->value_type); | ||
| DORIS_CHECK(data_type != nullptr); | ||
| if (is_complex_type(data_type->get_primitive_type())) { | ||
| return Status::OK(); | ||
|
|
@@ -218,7 +224,7 @@ ZoneMapFilterResult VInPredicate::evaluate_bloom_filter(const BloomFilterEvalCon | |
|
|
||
| bool VInPredicate::can_evaluate_bloom_filter() const { | ||
| return _zonemap_materialized && !_is_not_in && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Materialize nested IN probes before checking Bloom capability This new accessor-aware check still requires |
||
| std::dynamic_pointer_cast<VSlotRef>(get_child(0)) != nullptr; | ||
| expr_zonemap::extract_bloom_filter_probe(get_child(0)).has_value(); | ||
| } | ||
|
|
||
| bool VInPredicate::can_execute_on_raw_fixed_values(const DataTypePtr& data_type, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Connect LIST-to-STRUCT probes to production localization
The recursive extractor accepts a
LIST_ELEMENT -> STRUCT_FIELDpath, butTableColumnMapper::collect_struct_element_chain()rejects a struct accessor whose parent is the computed array element. Thuselement_at(element_at(items, 1), 'a') = 7produces no file-local conjunct even when the table and fileARRAY<STRUCT<a: INT>>schemas are identical, and this Parquet Bloom path is never reached; the existingArrayWrapperDoesNotBuildNestedPredicateFiltertest confirms that request is empty. Please add a schema-safe localization path plus a mapper-to-Parquet regression, or narrow the advertised recursive LIST/STRUCT support to the path shapes that can actually reach the reader.