Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
447 changes: 432 additions & 15 deletions be/benchmark/benchmark_hybrid_set.hpp

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions be/benchmark/benchmark_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "benchmark_fastunion.hpp"
#include "benchmark_fmod.hpp"
#include "benchmark_hll_merge.hpp"
#include "benchmark_hybrid_set.hpp"
#include "benchmark_json_extract.hpp"
#include "benchmark_zone_map_index.hpp"
#include "binary_cast_benchmark.hpp"
Expand Down
100 changes: 97 additions & 3 deletions be/src/exprs/bitset_container.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#pragma once

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
Expand Down Expand Up @@ -100,14 +101,24 @@ class BitSetContainer {
mutable T _cached_value = T();
};

BitSetContainer() { _data.fill(false); }
BitSetContainer() = default;

~BitSetContainer() = default;

ALWAYS_INLINE void insert(const T& value) {
auto idx = _to_index(value);
if (!_data[idx]) {
_data[idx] = true;
const size_t block = idx / VALUES_PER_BLOCK;
_nonempty_blocks[block / SUMMARY_WORD_BITS] |= uint64_t {1}
<< (block % SUMMARY_WORD_BITS);
if (_size == 0) {
_min_value = value;
_max_value = value;
} else {
_min_value = std::min(_min_value, value);
_max_value = std::max(_max_value, value);
}
_size++;
}
}
Expand All @@ -116,10 +127,33 @@ class BitSetContainer {

void clear() {
_data.fill(false);
_nonempty_blocks.fill(0);
_size = 0;
}

size_t size() const { return _size; }
T min_value() const { return _min_value; }
T max_value() const { return _max_value; }

template <typename Less>
bool contains_any_in_range(T min_value, T max_value, Less&& less) const {
if (_size == 0) {
return false;
}
min_value = std::max(min_value, _min_value);
max_value = std::min(max_value, _max_value);
if (less(max_value, min_value)) {
return false;
}

const size_t min_index = _to_index(min_value);
const size_t max_index = _to_index(max_value);
if (min_value < 0 && max_value >= 0) {
return _has_value_in_raw_index_range(min_index, RANGE - 1) ||
_has_value_in_raw_index_range(0, max_index);
}
return _has_value_in_raw_index_range(min_index, max_index);
}

Iterator begin() const { return Iterator(&_data, 0); }
Iterator end() const { return Iterator(&_data, RANGE); }
Expand All @@ -141,6 +175,63 @@ class BitSetContainer {
}

private:
static constexpr size_t VALUES_PER_BLOCK = 64;
static constexpr size_t SUMMARY_WORD_BITS = 64;
static constexpr size_t BLOCK_COUNT = (RANGE + VALUES_PER_BLOCK - 1) / VALUES_PER_BLOCK;
static constexpr size_t SUMMARY_WORD_COUNT =
(BLOCK_COUNT + SUMMARY_WORD_BITS - 1) / SUMMARY_WORD_BITS;

bool _has_nonempty_block_in_range(size_t first_block, size_t last_block) const {
const size_t first_word = first_block / SUMMARY_WORD_BITS;
const size_t last_word = last_block / SUMMARY_WORD_BITS;
const size_t first_bit = first_block % SUMMARY_WORD_BITS;
const size_t last_bit = last_block % SUMMARY_WORD_BITS;
const uint64_t first_mask = ~uint64_t {0} << first_bit;
const uint64_t last_mask = last_bit == SUMMARY_WORD_BITS - 1
? ~uint64_t {0}
: (uint64_t {1} << (last_bit + 1)) - uint64_t {1};
if (first_word == last_word) {
return (_nonempty_blocks[first_word] & first_mask & last_mask) != 0;
}
if ((_nonempty_blocks[first_word] & first_mask) != 0) {
return true;
}
for (size_t word = first_word + 1; word < last_word; ++word) {
if (_nonempty_blocks[word] != 0) {
return true;
}
}
return (_nonempty_blocks[last_word] & last_mask) != 0;
}

bool _has_value_in_raw_index_range(size_t first, size_t last) const {
const size_t first_block = first / VALUES_PER_BLOCK;
const size_t last_block = last / VALUES_PER_BLOCK;
if (first_block == last_block) {
return _has_value_in_boundary_range(first, last);
}

const size_t first_block_end = (first_block + 1) * VALUES_PER_BLOCK;
if (_has_value_in_boundary_range(first, first_block_end - 1)) {
return true;
}
const size_t last_block_start = last_block * VALUES_PER_BLOCK;
if (_has_value_in_boundary_range(last_block_start, last)) {
return true;
}
return first_block + 1 < last_block &&
_has_nonempty_block_in_range(first_block + 1, last_block - 1);
}

bool _has_value_in_boundary_range(size_t first, size_t last) const {
for (size_t index = first; index <= last; ++index) {
if (_data[index]) {
return true;
}
}
return false;
}

ALWAYS_INLINE constexpr size_t _to_index(T value) const {
if constexpr (std::is_same_v<T, int8_t>) {
return static_cast<uint8_t>(value);
Expand All @@ -149,8 +240,11 @@ class BitSetContainer {
}
}

std::array<bool, RANGE> _data;
std::array<bool, RANGE> _data {};
std::array<uint64_t, SUMMARY_WORD_COUNT> _nonempty_blocks {};
size_t _size = 0;
T _min_value {};
T _max_value {};
};

} // namespace doris
} // namespace doris
136 changes: 68 additions & 68 deletions be/src/exprs/expr_zonemap_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ std::optional<std::pair<Field, DataTypePtr>> field_from_literal_expr(const VExpr
return std::make_pair(std::move(field), literal->get_data_type());
}

bool value_in_range(const Field& value, const Field& min_value, const Field& max_value) {
return value >= min_value && value <= max_value;
}

bool dictionary_contains(const DictionaryEvalContext::SlotDictionary& dictionary,
const Field& value) {
return std::ranges::any_of(dictionary.values, [&](const Field& dictionary_value) {
Expand Down Expand Up @@ -143,40 +139,24 @@ TExprNode create_texpr_node_from_hybrid_set_value(const void* data, const Primit
return create_texpr_node_from(data, type, precision, scale);
}

Status materialize_hybrid_set_for_zonemap_filter(HybridSetBase& set, const DataTypePtr& data_type,
InZonemapMaterializedSet* result) {
DORIS_CHECK(result != nullptr);
void get_hybrid_set_min_max_for_zonemap_filter(const std::shared_ptr<HybridSetBase>& set,
const DataTypePtr& data_type,
InZonemapMinMax& result) {
DORIS_CHECK(set != nullptr);
DORIS_CHECK(data_type != nullptr);
const auto value_type = remove_nullable(data_type);
DORIS_CHECK(value_type != nullptr);

result->contains_null = set.contain_null();
result->values.clear();
result->min_value = Field();
result->max_value = Field();

auto* iterator = set.begin();
while (iterator->has_next()) {
const void* value = iterator->get_value();
if (value != nullptr) {
TExprNode literal_node = create_texpr_node_from_hybrid_set_value(
value, value_type->get_primitive_type(), value_type->get_precision(),
value_type->get_scale());
auto literal = VLiteral::create_shared(literal_node);
Field field;
literal->get_column_ptr()->get(0, field);
result->values.emplace_back(std::move(field));
}
iterator->next();
}

if (!result->values.empty()) {
auto minmax = std::ranges::minmax_element(
result->values, [](const Field& lhs, const Field& rhs) { return lhs < rhs; });
result->min_value = *minmax.min;
result->max_value = *minmax.max;
set->get_min_max(result.min_value, result.max_value);
const auto value_count = set->size();
if (value_count != 0) {
DORIS_CHECK(!result.min_value.is_null());
DORIS_CHECK(!result.max_value.is_null());
DORIS_CHECK(field_types_compatible(result.min_value.get_type(),
value_type->get_primitive_type()));
DORIS_CHECK(field_types_compatible(result.max_value.get_type(),
value_type->get_primitive_type()));
}
return Status::OK();
}

std::optional<SlotLiteral> extract_slot_and_literal(const VExprSPtrs& args) {
Expand Down Expand Up @@ -243,23 +223,30 @@ ZoneMapFilterResult eval_null_zonemap(const ZoneMapEvalContext& ctx, const VExpr
}

ZoneMapFilterResult eval_in_zonemap(const ZoneMapEvalContext& ctx, const VExprSPtr& slot_expr,
bool is_not_in, const std::vector<Field>& values,
const Field& min_value, const Field& max_value) {
bool is_not_in, const InZonemapMinMax& values,
const HybridSetBase& set) {
auto slot = std::dynamic_pointer_cast<VSlotRef>(slot_expr);
DORIS_CHECK(slot != nullptr);
// NOT IN with a NULL literal is UNKNOWN for every non-null value. Zone maps do not retain
// enough row-level information to recover a match in that case.
if (is_not_in && set.contain_null()) {
return ZoneMapFilterResult::kNoMatch;
}
// Empty IN has no candidate values, while NOT IN with an empty set cannot filter anything.
if (values.empty()) {
if (set.size() == 0) { // NOLINT(readability-container-size-empty)
return is_not_in ? ZoneMapFilterResult::kMayMatch : ZoneMapFilterResult::kNoMatch;
}

// The caller has materialized the IN set and precomputed its non-null min/max. They must match
// The caller has precomputed the IN set's owning non-null min/max. They must match
// the expression slot type before being compared with storage zone-map statistics.
DORIS_CHECK(!min_value.is_null());
DORIS_CHECK(!max_value.is_null());
DORIS_CHECK(!values.min_value.is_null());
DORIS_CHECK(!values.max_value.is_null());
auto data_type = remove_nullable(slot->data_type());
DORIS_CHECK(data_type != nullptr);
DORIS_CHECK(field_types_compatible(min_value.get_type(), data_type->get_primitive_type()));
DORIS_CHECK(field_types_compatible(max_value.get_type(), data_type->get_primitive_type()));
DORIS_CHECK(
field_types_compatible(values.min_value.get_type(), data_type->get_primitive_type()));
DORIS_CHECK(
field_types_compatible(values.max_value.get_type(), data_type->get_primitive_type()));

// Re-check against the reader-schema type and the available zone map. Missing or unsupported
// metadata must conservatively fall back to may-match.
Expand All @@ -285,39 +272,36 @@ ZoneMapFilterResult eval_in_zonemap(const ZoneMapEvalContext& ctx, const VExprSP
// NOT IN can only prune when the whole zone contains exactly one non-null value and that
// value is excluded by the set. Wider ranges may contain values that are not filtered.
if (zone_map.min_value == zone_map.max_value) {
const bool only_value_is_filtered = std::ranges::any_of(
values, [&](const Field& value) { return value == zone_map.min_value; });
const bool only_value_is_filtered = set.find(zone_map.min_value);
return only_value_is_filtered ? ZoneMapFilterResult::kNoMatch
: ZoneMapFilterResult::kMayMatch;
}
return ZoneMapFilterResult::kMayMatch;
}

// First use the materialized IN-set min/max to rule out disjoint zone-map ranges.
if (zone_map.max_value < min_value || zone_map.min_value > max_value) {
// First use the IN-set min/max to rule out disjoint zone-map ranges.
if (zone_map.max_value < values.min_value || zone_map.min_value > values.max_value) {
return ZoneMapFilterResult::kNoMatch;
}

// For large IN sets, avoid checking every point on the scan hot path. The range overlap above
// is only a coarse may-match signal.
if (values.size() > kInZoneMapPointCheckThreshold) {
if (std::cmp_greater(set.size(), kInZoneMapPointCheckThreshold)) {
++ctx.stats.in_zonemap_range_only_count;
return ZoneMapFilterResult::kMayMatch;
}

// For small IN sets, verify whether any candidate value can fall into the zone-map range.
// Convert the two zone-map bounds to the HybridSet's native type once, then compare them
// directly with the typed set values without retaining a Field copy of every IN candidate.
++ctx.stats.in_zonemap_point_check_count;
for (const auto& value : values) {
if (value_in_range(value, zone_map.min_value, zone_map.max_value)) {
return ZoneMapFilterResult::kMayMatch;
}
}
return ZoneMapFilterResult::kNoMatch;
return set.contains_any_in_range(zone_map.min_value, zone_map.max_value)
? ZoneMapFilterResult::kMayMatch
: ZoneMapFilterResult::kNoMatch;
}

ZoneMapFilterResult eval_eq_dictionary(const DictionaryEvalContext& ctx,
const SlotLiteral& slot_literal) {
auto dictionary = ctx.slot(slot_literal.slot_index);
const auto* dictionary = ctx.slot(slot_literal.slot_index);
if (dictionary == nullptr || dictionary->data_type == nullptr) {
return ZoneMapFilterResult::kUnsupported;
}
Expand All @@ -330,22 +314,24 @@ ZoneMapFilterResult eval_eq_dictionary(const DictionaryEvalContext& ctx,
}

ZoneMapFilterResult eval_in_dictionary(const DictionaryEvalContext& ctx, const VExprSPtr& slot_expr,
bool is_not_in, const std::vector<Field>& values) {
bool is_not_in, const HybridSetBase& values) {
if (is_not_in) {
return ZoneMapFilterResult::kUnsupported;
}
auto slot = std::dynamic_pointer_cast<VSlotRef>(slot_expr);
DORIS_CHECK(slot != nullptr);
auto dictionary = ctx.slot(slot->column_id());
const auto* dictionary = ctx.slot(slot->column_id());
if (dictionary == nullptr || dictionary->data_type == nullptr) {
return ZoneMapFilterResult::kUnsupported;
}
DORIS_CHECK(data_types_compatible(dictionary->data_type, slot->data_type()));
if (values.empty()) {
// HybridSetBase::empty() also treats a NULL literal as non-empty, but dictionary pruning needs
// to know whether there are any non-NULL candidates.
if (values.size() == 0) { // NOLINT(readability-container-size-empty)
return ZoneMapFilterResult::kNoMatch;
}
for (const auto& value : values) {
if (!value.is_null() && dictionary_contains(*dictionary, value)) {
for (const auto& value : dictionary->values) {
if (!value.is_null() && values.find(value)) {
return ZoneMapFilterResult::kMayMatch;
}
}
Expand All @@ -354,7 +340,7 @@ ZoneMapFilterResult eval_in_dictionary(const DictionaryEvalContext& ctx, const V

ZoneMapFilterResult eval_eq_bloom_filter(const BloomFilterEvalContext& ctx,
const SlotLiteral& slot_literal) {
auto slot_filter = ctx.slot(slot_literal.slot_index);
const auto* slot_filter = ctx.slot(slot_literal.slot_index);
if (slot_filter == nullptr || slot_filter->data_type == nullptr ||
slot_filter->bloom_filter == nullptr) {
return ZoneMapFilterResult::kUnsupported;
Expand All @@ -370,27 +356,41 @@ ZoneMapFilterResult eval_eq_bloom_filter(const BloomFilterEvalContext& ctx,

ZoneMapFilterResult eval_in_bloom_filter(const BloomFilterEvalContext& ctx,
const VExprSPtr& slot_expr, bool is_not_in,
const std::vector<Field>& values) {
const HybridSetBase& values) {
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());
const auto* slot_filter = ctx.slot(slot->column_id());
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()));
if (values.empty()) {
if (values.size() == 0) { // NOLINT(readability-container-size-empty)
return ZoneMapFilterResult::kNoMatch;
}
for (const auto& value : values) {
if (!value.is_null() && bloom_filter_may_contain(*slot_filter, value)) {
return ZoneMapFilterResult::kMayMatch;
}
const auto value_type = remove_nullable(slot_filter->data_type)->get_primitive_type();
switch (value_type) {
case TYPE_BOOLEAN:
case TYPE_INT:
case TYPE_BIGINT:
case TYPE_FLOAT:
case TYPE_DOUBLE:
case TYPE_CHAR:
case TYPE_VARCHAR:
case TYPE_STRING:
break;
default:
return ZoneMapFilterResult::kMayMatch;
}
return ZoneMapFilterResult::kNoMatch;
return values.any_match_raw(value_type,
[slot_filter](const char* data, size_t size) {
return slot_filter->bloom_filter->test_bytes(data, size);
})
? ZoneMapFilterResult::kMayMatch
: ZoneMapFilterResult::kNoMatch;
}

// Return the only slot ordinal referenced by a zonemap-evaluable expression. A negative result is
Expand Down
Loading
Loading