github-actions[bot] commented on code in PR #66036:
URL: https://github.com/apache/doris/pull/66036#discussion_r3649660102
##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -1392,11 +1394,172 @@ uint16_t count_selected_rows(const IColumn::Filter&
filter) {
return selected_rows;
}
-IColumn::Filter build_dictionary_entry_filter(size_t block_position,
- const ParquetColumnSchema&
column_schema,
- const VExprContextSPtrs&
conjuncts,
- const IColumn& dictionary) {
- IColumn::Filter dictionary_filter(dictionary.size(), 1);
+enum class DictionaryEntryFilterKernel {
+ GENERIC,
+ TYPED_FIXED_WIDTH,
+ TYPED_STRING,
+};
+
+template <typename ColumnType>
+bool get_fixed_dictionary_raw_values(const IColumn& dictionary, const
uint8_t** values,
+ size_t* value_width) {
+ const auto* typed_dictionary =
check_and_get_column<ColumnType>(dictionary);
+ if (typed_dictionary == nullptr) {
+ return false;
+ }
+ *values = reinterpret_cast<const
uint8_t*>(typed_dictionary->get_data().data());
+ *value_width = sizeof(typename ColumnType::value_type);
+ return true;
+}
+
+bool get_numeric_dictionary_raw_values(PrimitiveType primitive_type, const
IColumn& dictionary,
+ const uint8_t** values, size_t*
value_width) {
+ switch (primitive_type) {
+ case TYPE_INT:
+ return get_fixed_dictionary_raw_values<ColumnInt32>(dictionary,
values, value_width);
+ case TYPE_BIGINT:
+ return get_fixed_dictionary_raw_values<ColumnInt64>(dictionary,
values, value_width);
+ case TYPE_FLOAT:
Review Comment:
[P2] Add end-to-end FLOAT/DOUBLE dictionary-filter coverage
This admits FLOAT and DOUBLE to an exact bitmap path whose covered conjunct
may be removed, but the new comparison scans exercise only INT32, BIGINT, and
BYTE_ARRAY. The pre-existing accept-all dictionary scan asserts only row
count/broad counters, and the raw NaN unit test does not cover FLOAT/DOUBLE ID
filtering, nullable placement, or fused projection. Please add differential
FLOAT/DOUBLE comparison scans for projected and predicate-only modes, including
NaN, signed zero, infinities, NULLs, and operand reversal, and assert output
values plus direct-path counters.
##########
be/src/format_v2/parquet/reader/native_column_reader.cpp:
##########
@@ -395,6 +381,73 @@ Status
NativeColumnReader::read_with_fixed_width_filter(int64_t rows, const uint
return Status::OK();
}
+Status NativeColumnReader::read_with_dictionary_filter(
+ int64_t rows, const uint8_t* filter_data, bool filter_all,
+ const IColumn::Filter& dictionary_filter, const IColumn*
typed_dictionary,
+ IColumn* projected_values, ColumnInt32* matched_dictionary_ids,
IColumn::Filter* row_filter,
+ int64_t* rows_read, bool* projected_directly, bool* used_filter) {
+ DORIS_CHECK(rows >= 0);
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(rows_read != nullptr);
+ DORIS_CHECK(projected_directly != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ row_filter->clear();
+ *rows_read = 0;
+ *projected_directly = false;
+ *used_filter = false;
+ if (rows == 0) {
+ return Status::OK();
+ }
+
+ native::FilterMap filter;
+ RETURN_IF_ERROR(filter.init(filter_data, static_cast<size_t>(rows),
filter_all));
+ _native_reader->reset_filter_map_index();
+ bool eof = false;
+ int64_t consecutive_empty_calls = 0;
+ while (*rows_read < rows && !eof) {
+ size_t loop_rows = 0;
+ IColumn::Filter loop_filter;
+ bool loop_projected_directly = false;
+ bool loop_used = false;
+ RETURN_IF_ERROR(_native_reader->read_dictionary_filter(
+ dictionary_filter, filter, static_cast<size_t>(rows -
*rows_read), typed_dictionary,
+ projected_values, matched_dictionary_ids, &loop_filter,
&loop_rows, &eof,
+ &loop_projected_directly, &loop_used));
+ if (!loop_used) {
+ if (UNLIKELY(*rows_read != 0)) {
+ return Status::Corruption(
+ "Parquet dictionary predicate encoding changed after
{} rows for column {}",
+ *rows_read, _name);
+ }
+ row_filter->clear();
+ return Status::OK();
+ }
+ if (*rows_read != 0) {
+ DORIS_CHECK_EQ(*projected_directly, loop_projected_directly);
Review Comment:
[P1] Keep empty page transitions projection-mode neutral
A zero-value Data Page V1/V2 is a valid input that the native reader already
skips. Here, after an earlier dictionary page has appended fixed-width
survivors and set `projected_directly=true`, the zero-row page returns
`loop_rows=0`, `used_filter=true`, and the default
`loop_projected_directly=false`, so this check terminates the BE before the
following page is read. This is distinct from the all-NULL-page thread because
the zero-row page consumes no definition/value cursor. Please compare/update
projection mode only for fragments that process logical rows, and add projected
RLE_DICTIONARY/PLAIN_DICTIONARY V1/V2 coverage with a zero-value page between
nonempty pages.
##########
be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp:
##########
@@ -1712,6 +1712,201 @@ Status ColumnChunkReader<IN_COLLECTION,
OFFSET_INDEX>::filter_fixed_width_values
return Status::OK();
}
+namespace {
+
+template <typename ColumnType>
+bool try_filter_and_project_dictionary_values(
+ const IColumn* typed_dictionary, IColumn* projected_values,
+ const std::vector<uint32_t>& selected_dictionary_indices,
+ const NullMap& nullable_selection_nulls, const IColumn::Filter&
dictionary_filter,
+ IColumn::Filter* row_filter) {
+ if (typed_dictionary == nullptr || projected_values == nullptr) {
+ return false;
+ }
+ const auto* dictionary =
check_and_get_column<ColumnType>(*typed_dictionary);
+ auto* projected = check_and_get_column<ColumnType>(*projected_values);
+ if (dictionary == nullptr || projected == nullptr) {
+ return false;
+ }
+
+ const auto& dictionary_data = dictionary->get_data();
+ auto& projected_data = projected->get_data();
+ projected_data.reserve(projected_data.size() +
selected_dictionary_indices.size());
+ row_filter->reserve(nullable_selection_nulls.size());
+ size_t physical_row = 0;
+ for (const uint8_t is_null : nullable_selection_nulls) {
+ bool keep = false;
+ if (is_null == 0) {
+ const uint32_t dictionary_id =
selected_dictionary_indices[physical_row++];
+ // The decoder validates the complete id batch first, preserving
atomic output while
+ // allowing this hot gather loop to use unchecked dictionary
lookups.
+ keep = dictionary_filter[dictionary_id] != 0;
+ if (keep) {
+ projected_data.push_back(dictionary_data[dictionary_id]);
+ }
+ }
+ row_filter->push_back(keep ? 1 : 0);
+ }
+ DORIS_CHECK_EQ(physical_row, selected_dictionary_indices.size());
+ return true;
+}
+
+bool try_filter_and_project_fixed_width_dictionary(
+ const IColumn* typed_dictionary, IColumn* projected_values,
+ const std::vector<uint32_t>& selected_dictionary_indices,
+ const NullMap& nullable_selection_nulls, const IColumn::Filter&
dictionary_filter,
+ IColumn::Filter* row_filter) {
+#define TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnType)
\
+ if (try_filter_and_project_dictionary_values<ColumnType>(
\
+ typed_dictionary, projected_values,
selected_dictionary_indices, \
+ nullable_selection_nulls, dictionary_filter, row_filter)) {
\
+ return true;
\
+ }
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnUInt8)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnInt8)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnInt16)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnInt32)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnInt64)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnInt128)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnFloat32)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnFloat64)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDate)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDateTime)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDateV2)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDateTimeV2)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnTimeV2)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnTimeStampTz)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnIPv4)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnIPv6)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnOffset32)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnOffset64)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDecimal32)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDecimal64)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDecimal128V2)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDecimal128V3)
+ TRY_FIXED_WIDTH_DICTIONARY_COLUMN(ColumnDecimal256)
+#undef TRY_FIXED_WIDTH_DICTIONARY_COLUMN
+ return false;
+}
+
+} // namespace
+
+template <bool IN_COLLECTION, bool OFFSET_INDEX>
+Status ColumnChunkReader<IN_COLLECTION,
OFFSET_INDEX>::filter_dictionary_indices(
+ const IColumn::Filter& dictionary_filter, ColumnSelectVector&
select_vector,
+ const IColumn* typed_dictionary, IColumn* projected_values,
+ ColumnInt32* matched_dictionary_ids, IColumn::Filter* row_filter,
bool* projected_directly,
+ bool* used_filter) {
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(projected_directly != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ DORIS_CHECK((typed_dictionary == nullptr) == (projected_values ==
nullptr));
+ *projected_directly = false;
+ *used_filter = false;
+ row_filter->clear();
+ if (_current_encoding != tparquet::Encoding::RLE_DICTIONARY ||
_page_decoder == nullptr ||
+ !_page_decoder->has_dictionary()) {
+ return Status::OK();
+ }
+ if (UNLIKELY(_remaining_num_values < select_vector.num_values())) {
+ return Status::IOError("Decode too many values in current page");
+ }
+ if (UNLIKELY(dictionary_filter.size() !=
_page_decoder->dictionary_size())) {
+ return Status::Corruption("Parquet predicate dictionary has {}
entries, expected {}",
+ dictionary_filter.size(),
_page_decoder->dictionary_size());
+ }
+
+ ParquetSelection selection;
Review Comment:
[P2] Reuse sparse selection-range scratch
A staged earlier predicate can leave one disjoint physical run per surviving
row. This local `ParquetSelection` then grows `ranges` to that fragmentation
and destroys the capacity at the end of every page fragment, so later
dictionary predicates repeatedly allocate/free the same range storage. The
mandatory scanner guide requires selection ranges to live in persistent reader
scratch specifically to avoid this hot-path allocation thrash. Please retain
and clear a reusable allocator-aware selection (including its capacity in the
existing active/retained scratch policy), and cover a two-predicate
fragmented-selection benchmark.
##########
be/src/format_v2/parquet/reader/native/column_chunk_reader.h:
##########
@@ -408,6 +420,7 @@ class ColumnChunkReader {
// Plain or Dictionary encoding. If the dictionary grows too big, the
encoding will fall back to the plain encoding
std::unordered_map<int, std::unique_ptr<Decoder>> _decoders;
NullMap _nullable_selection_nulls;
+ std::vector<uint32_t> _selected_dictionary_indices;
Review Comment:
[P2] Track the retained selected-ID scratch
This is a persistent per-leaf buffer, but `std::vector` growth does not pass
through Doris `Allocator::consume_memory`; the checked-in malloc interposer
only forwards allocations to jemalloc. The retained-byte bookkeeping here
drives the scratch-release policy, not the query MemTracker. With the uint16_t
batch bound this can retain about 256 KiB per dictionary predicate leaf, and
the 4 MiB per-buffer release threshold means that normal maximum capacity
survives until reader teardown, multiplying across wide scans. Please use
allocator-aware storage such as `DorisVector` (updating the decoder interface)
or explicitly charge/release capacity to the owning query tracker.
##########
be/benchmark/parquet/parquet_benchmark_scenarios.h:
##########
@@ -326,11 +354,12 @@ inline std::string to_string(ReaderOperation value) {
}
inline std::string reader_scenario_name(const ReaderScenario& scenario) {
- return to_string(scenario.operation) + "/" + to_string(scenario.encoding)
+ "/null_" +
- std::to_string(scenario.null_percent) + "/" +
to_string(scenario.null_pattern) +
- "/sel_" + std::to_string(scenario.selectivity_percent) + "/" +
- to_string(scenario.projection) + "/width_" +
std::to_string(scenario.schema_width) +
- "/predicate_" + std::to_string(scenario.predicate_position);
+ return to_string(scenario.operation) + "/" + to_string(scenario.encoding)
+ "/" +
Review Comment:
[P2] Update the exact benchmark filters for the new name shape
Adding `/<value_type>/` changes every reader registration to
`.../<encoding>/<value_type>/null_...`, but the exact filters in
`be/benchmark/parquet/AGENTS.md:93` and `be/benchmark/parquet/README.md:75,87`
still use `.../<encoding>/null_...` and now select no cases. This is distinct
from the existing stale-count/matrix thread: correcting those descriptions
still leaves the documented comparisons as no-ops. Please update all exact
filters and validate them with `--benchmark_list_tests`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]