This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 72989876caa [improvement](parquet) Lazily materialize complex residual
columns (#65965)
72989876caa is described below
commit 72989876caa12a025d74c1d80b5469a25df4b421
Author: Gabriel <[email protected]>
AuthorDate: Fri Jul 24 11:24:37 2026 +0800
[improvement](parquet) Lazily materialize complex residual columns (#65965)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #65921
Problem Summary:
The V2 Parquet scanner already evaluates single-column predicates round
by round, but a multi-column residual (including the children of a
compound `AND`) still caused every predicate column to be materialized
before expression evaluation. Expression short circuiting therefore
happened after Parquet decode/IO and could not avoid later-only columns.
This PR follows expression-triggered lazy materialization:
- preserve conjunct order and split safe compound `AND` residuals into
ordered expression stages;
- record each stage's slot dependencies and materialize only the next
reachable columns;
- read later columns with the surviving selection, or skip them entirely
when an earlier stage rejects the batch;
- prefetch only the first reachable predicate stage;
- retain the original eager path for stateful/error-sensitive
expressions that are unsafe on selected rows;
- add unit coverage and a Release microbenchmark scenario based on the
Parquet benchmark framework from #65921.
Correctness/counter validation (6 input rows):
- an earlier two-column `AND` child filters all rows:
`ReaderReadRows=12`, `ReaderSkipRows=6` (the third column is never
decoded);
- when 3 rows survive the first residual: the later column reads only
those 3 rows (`ReaderReadRows=15`, `ReaderSelectRows=3`,
`ReaderSkipRows=3`).
Release microbenchmark:
```text
CPU: Intel Xeon Platinum 8457C, pinned to CPU 8
Build: RELEASE
Filter:
^ParquetReader/complex_residual_scan/plain/null_10/alternating/sel_10/
Protocol: 3 warmups, then A-B-B-A; 5 repetitions per group; min_time=1s
Pair 1 median CPU: before 1,377,041 ns; after 1,318,811 ns (-4.23%)
Pair 2 median CPU: before 2,140,937 ns; after 2,124,862 ns (-0.75%)
Paired geometric normalization: -2.50% CPU time
Selected rows: 1,460 in every run
```
The host had high concurrent load and CPU scaling enabled, so the paired
groups and run conditions are reported explicitly rather than relying on
wall time. Both A/B pairs improve in the same direction.
### Release note
None
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [x] Manual test (Release microbenchmark above)
- [ ] No need to test or manual test.
- Behavior changed:
- [x] No. The SQL result is unchanged; only predicate-column
materialization timing changes.
- [ ] Yes.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/benchmark/parquet/AGENTS.md | 10 +-
be/benchmark/parquet/README.md | 15 +-
be/benchmark/parquet/benchmark_parquet_reader.hpp | 94 +++++++-
be/benchmark/parquet/parquet_benchmark_scenarios.h | 22 +-
be/src/format_v2/expr/cast.h | 3 +
be/src/format_v2/parquet/parquet_profile.cpp | 3 +
be/src/format_v2/parquet/parquet_profile.h | 2 +
be/src/format_v2/parquet/parquet_scan.cpp | 251 +++++++++++++++++----
be/src/format_v2/parquet/parquet_scan.h | 11 +-
.../parquet/parquet_benchmark_scenarios_test.cpp | 24 +-
be/test/format_v2/parquet/parquet_scan_test.cpp | 241 +++++++++++++++++++-
11 files changed, 613 insertions(+), 63 deletions(-)
diff --git a/be/benchmark/parquet/AGENTS.md b/be/benchmark/parquet/AGENTS.md
index a8667fb43cd..a558f69937c 100644
--- a/be/benchmark/parquet/AGENTS.md
+++ b/be/benchmark/parquet/AGENTS.md
@@ -42,7 +42,7 @@ be/output/lib/benchmark_test --benchmark_list_tests \
| grep -c '^ParquetDecoder/' # currently 152
be/output/lib/benchmark_test --benchmark_list_tests \
- | grep -c '^ParquetReader/' # currently 137
+ | grep -c '^ParquetReader/' # currently 152
```
When running the binary directly from `be/build_RELEASE/bin`, make sure the
JVM and third-party
@@ -111,9 +111,10 @@ selection with clustered and alternating selection ranges,
for 152 registered ca
| DELTA_BYTE_ARRAY | BYTE_ARRAY |
`ParquetReader` deliberately uses a single-variable matrix rather than a
Cartesian product. After
-deduplication it contains 137 cases covering:
+deduplication it contains 152 cases covering:
-- operations: open-to-first-block, full scan, predicate scan, limit 1, and
limit 1000;
+- operations: open-to-first-block, full scan, predicate scan, complex residual
scan, limit 1, and
+ limit 1000;
- file encodings: PLAIN, dictionary, BYTE_STREAM_SPLIT, and
DELTA_BINARY_PACKED;
- null ratios: 0%, 1%, 10%, 50%, and 90%;
- null shapes: clustered and alternating;
@@ -167,6 +168,9 @@ Fixture contents and writer settings are:
- every non-null value is `row % 100`;
- the predicate is `value < selectivity_percent`, so the threshold maps
directly to the intended
non-null selectivity;
+- the complex residual scan evaluates a production expression tree whose first
child is
+ `c0 < selectivity_percent` and whose always-true second child is `c2 = c3`,
exposing whether
+ later-only columns are decoded eagerly;
- alternating nulls use a 101-row period and `(row * 37) % 101`, avoiding
direct correlation with
the 100-value predicate period;
- clustered nulls use contiguous null prefixes inside each 1,024-row cluster;
diff --git a/be/benchmark/parquet/README.md b/be/benchmark/parquet/README.md
index 40c79bc28be..f024f7d6a24 100644
--- a/be/benchmark/parquet/README.md
+++ b/be/benchmark/parquet/README.md
@@ -36,13 +36,14 @@ be/output/lib/benchmark_test \
## Local reader cases
-`ParquetReader` measures local open-to-first-block, full scan, predicate scan,
and LIMIT-shaped
-reads. The matrix covers:
+`ParquetReader` measures local open-to-first-block, full scan, predicate scan,
complex residual
+scan, and LIMIT-shaped reads. The matrix covers:
- PLAIN, dictionary, byte-stream-split, and DELTA binary-packed files;
- NULL ratios of 0%, 1%, 10%, 50%, and 90%, with clustered and alternating
placement;
- predicate selectivities of 0%, 1%, 10%, 50%, 90%, and 100%;
- predicate-only and predicate-plus-lazy-projected reads;
+- ordered complex residuals whose later columns are reachable only after an
earlier residual;
- schemas with 4, 32, 128, and 512 columns, with the predicate first or last.
Fixtures are created lazily under the system temporary directory in
@@ -58,6 +59,16 @@ be/output/lib/benchmark_test \
--benchmark_out_format=json
```
+The complex-residual case uses a production compound `AND` tree. Its first
child,
+`c0 < selectivity_percent`, preserves the requested selectivity; its second
child, `c2 = c3`,
+references two new columns and accepts every row that reaches it:
+
+```shell
+be/output/lib/benchmark_test \
+
--benchmark_filter='^ParquetReader/complex_residual_scan/plain/null_10/alternating/sel_10/'
\
+ --benchmark_min_time=1s
+```
+
Every result reports throughput plus `raw_rows`, `selected_rows`,
`fixture_bytes`, `ns/raw_row`,
and (when at least one row survives) `ns/selected_row`. Keep CPU frequency,
build type, compiler,
machine placement, and benchmark filters fixed when comparing two commits.
diff --git a/be/benchmark/parquet/benchmark_parquet_reader.hpp
b/be/benchmark/parquet/benchmark_parquet_reader.hpp
index ae3e14c0f66..63094738040 100644
--- a/be/benchmark/parquet/benchmark_parquet_reader.hpp
+++ b/be/benchmark/parquet/benchmark_parquet_reader.hpp
@@ -24,6 +24,7 @@
#include <parquet/arrow/writer.h>
#include <algorithm>
+#include <array>
#include <cstdint>
#include <filesystem>
#include <memory>
@@ -39,13 +40,18 @@
#include "core/column/column_vector.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
+#include "exprs/vcompound_pred.h"
+#include "exprs/vectorized_fn_call.h"
#include "exprs/vexpr.h"
#include "exprs/vexpr_context.h"
+#include "exprs/vliteral.h"
+#include "exprs/vslot_ref.h"
#include "format_v2/file_reader.h"
#include "format_v2/parquet/parquet_reader.h"
#include "gen_cpp/Types_types.h"
#include "io/io_common.h"
#include "parquet_benchmark_scenarios.h"
+#include "runtime/descriptors.h"
#include "runtime/runtime_state.h"
#include "storage/index/zone_map/zonemap_eval_context.h"
#include "storage/index/zone_map/zonemap_filter_result.h"
@@ -275,6 +281,55 @@ inline VExprContextSPtr make_predicate(int
column_position, int selectivity_perc
return context;
}
+inline VExprSPtr make_int32_comparison(const std::string& function_name,
TExprOpcode::type opcode,
+ VExprSPtr left, VExprSPtr right) {
+ const auto bool_type = make_nullable(std::make_shared<DataTypeUInt8>());
+ TFunctionName name;
+ name.__set_function_name(function_name);
+ TFunction function;
+ function.__set_name(name);
+ function.__set_binary_type(TFunctionBinaryType::BUILTIN);
+ function.__set_arg_types({left->data_type()->to_thrift(),
right->data_type()->to_thrift()});
+ function.__set_ret_type(bool_type->to_thrift());
+ function.__set_has_var_args(false);
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::BINARY_PRED);
+ node.__set_opcode(opcode);
+ node.__set_type(bool_type->to_thrift());
+ node.__set_fn(function);
+ node.__set_num_children(2);
+ node.__set_is_nullable(true);
+ auto comparison = VectorizedFnCall::create_shared(node);
+ comparison->add_child(std::move(left));
+ comparison->add_child(std::move(right));
+ return comparison;
+}
+
+inline VExprContextSPtr make_complex_residual_predicate(int
selectivity_percent, int first_position,
+ int
later_left_position,
+ int
later_right_position,
+ const DataTypePtr&
int_type) {
+ const auto bool_type = make_nullable(std::make_shared<DataTypeUInt8>());
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+ node.__set_opcode(TExprOpcode::COMPOUND_AND);
+ node.__set_type(bool_type->to_thrift());
+ node.__set_num_children(2);
+ node.__set_is_nullable(true);
+ auto compound = VCompoundPred::create_shared(node);
+ compound->add_child(make_int32_comparison(
+ "lt", TExprOpcode::LT,
+ VSlotRef::create_shared(first_position, first_position, -1,
int_type, "c0"),
+ VLiteral::create_shared(remove_nullable(int_type),
+
Field::create_field<TYPE_INT>(selectivity_percent))));
+ compound->add_child(make_int32_comparison(
+ "eq", TExprOpcode::EQ,
+ VSlotRef::create_shared(later_left_position, later_left_position,
-1, int_type, "c2"),
+ VSlotRef::create_shared(later_right_position,
later_right_position, -1, int_type,
+ "c3")));
+ return VExprContext::create_shared(std::move(compound));
+}
+
inline Block make_block(const std::vector<format::ColumnDefinition>& schema) {
Block block;
for (const auto& column : schema) {
@@ -284,10 +339,17 @@ inline Block make_block(const
std::vector<format::ColumnDefinition>& schema) {
}
struct ReaderSession {
+ ~ReaderSession() {
+ for (const auto& context : opened_conjuncts) {
+ context->close();
+ }
+ }
+
RuntimeState runtime_state {TQueryOptions(), TQueryGlobals()};
std::unique_ptr<format::parquet::ParquetReader> reader;
std::vector<format::ColumnDefinition> schema;
std::shared_ptr<format::FileScanRequest> request;
+ VExprContextSPtrs opened_conjuncts;
};
inline std::unique_ptr<ReaderSession> open_reader(const std::filesystem::path&
path,
@@ -324,6 +386,27 @@ inline std::unique_ptr<ReaderSession> open_reader(const
std::filesystem::path& p
const auto predicate_position =
session->request->local_positions.at(predicate_id).value();
session->request->conjuncts.push_back(
make_predicate(static_cast<int>(predicate_position),
scenario.selectivity_percent));
+ } else if (scenario.operation == ReaderOperation::COMPLEX_RESIDUAL_SCAN) {
+ DORIS_CHECK(scenario.schema_width >= 5);
+ std::array<int, 3> predicate_columns {0, 2, 3};
+ std::array<int, 3> predicate_positions {};
+ for (size_t index = 0; index < predicate_columns.size(); ++index) {
+ const int column = predicate_columns[index];
+ const auto predicate_id = format::LocalColumnId(column);
+ throw_if_error(request_builder.add_predicate_column(predicate_id));
+ session->request->predicate_only_columns.push_back(predicate_id);
+ predicate_positions[index] =
+
static_cast<int>(session->request->local_positions.at(predicate_id).value());
+ }
+ throw_if_error(request_builder.add_non_predicate_column(
+ format::LocalColumnId(scenario.schema_width - 1)));
+ auto context = make_complex_residual_predicate(
+ scenario.selectivity_percent, predicate_positions[0],
predicate_positions[1],
+ predicate_positions[2], session->schema[0].type);
+ throw_if_error(context->prepare(&session->runtime_state,
RowDescriptor()));
+ throw_if_error(context->open(&session->runtime_state));
+ session->request->conjuncts.push_back(context);
+ session->opened_conjuncts.push_back(std::move(context));
} else {
throw_if_error(request_builder.add_non_predicate_column(format::LocalColumnId(0)));
if (scenario.schema_width > 1) {
@@ -383,6 +466,9 @@ inline int projected_columns(const ReaderScenario&
scenario) {
scenario.projection == Projection::PREDICATE_ONLY) {
return 1;
}
+ if (scenario.operation == ReaderOperation::COMPLEX_RESIDUAL_SCAN) {
+ return 4;
+ }
return std::min(2, scenario.schema_width);
}
@@ -428,13 +514,7 @@ inline void run_reader(benchmark::State& state,
ReaderScenario scenario) {
inline bool register_reader_benchmarks() {
for (const auto& scenario : reader_scenarios()) {
- std::string name =
- "ParquetReader/" + 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);
+ std::string name = "ParquetReader/" + reader_scenario_name(scenario);
benchmark::RegisterBenchmark(name.c_str(), [=](benchmark::State&
state) {
run_reader(state, scenario);
})->Unit(benchmark::kNanosecond);
diff --git a/be/benchmark/parquet/parquet_benchmark_scenarios.h
b/be/benchmark/parquet/parquet_benchmark_scenarios.h
index a01c955c6a2..1db6b3c8fd2 100644
--- a/be/benchmark/parquet/parquet_benchmark_scenarios.h
+++ b/be/benchmark/parquet/parquet_benchmark_scenarios.h
@@ -37,7 +37,14 @@ enum class Encoding {
enum class ValueType { INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY,
FIXED_LEN_BYTE_ARRAY };
enum class Pattern { CLUSTERED, ALTERNATING };
enum class Projection { PREDICATE_ONLY, PREDICATE_PROJECTED };
-enum class ReaderOperation { OPEN_TO_FIRST_BLOCK, FULL_SCAN, PREDICATE_SCAN,
LIMIT_1, LIMIT_1000 };
+enum class ReaderOperation {
+ OPEN_TO_FIRST_BLOCK,
+ FULL_SCAN,
+ PREDICATE_SCAN,
+ COMPLEX_RESIDUAL_SCAN,
+ LIMIT_1,
+ LIMIT_1000
+};
struct DecoderScenario {
Encoding encoding;
@@ -113,7 +120,8 @@ inline std::vector<ReaderScenario> reader_scenarios() {
.predicate_position = 0};
for (const auto operation :
{ReaderOperation::OPEN_TO_FIRST_BLOCK, ReaderOperation::FULL_SCAN,
- ReaderOperation::PREDICATE_SCAN, ReaderOperation::LIMIT_1,
ReaderOperation::LIMIT_1000}) {
+ ReaderOperation::PREDICATE_SCAN,
ReaderOperation::COMPLEX_RESIDUAL_SCAN,
+ ReaderOperation::LIMIT_1, ReaderOperation::LIMIT_1000}) {
auto scenario = baseline;
scenario.operation = operation;
add(scenario);
@@ -252,6 +260,8 @@ inline std::string to_string(ReaderOperation value) {
return "full_scan";
case ReaderOperation::PREDICATE_SCAN:
return "predicate_scan";
+ case ReaderOperation::COMPLEX_RESIDUAL_SCAN:
+ return "complex_residual_scan";
case ReaderOperation::LIMIT_1:
return "limit_1";
case ReaderOperation::LIMIT_1000:
@@ -260,4 +270,12 @@ inline std::string to_string(ReaderOperation value) {
return "unknown";
}
+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);
+}
+
} // namespace doris::parquet_benchmark
diff --git a/be/src/format_v2/expr/cast.h b/be/src/format_v2/expr/cast.h
index 1dc06bcf07f..22604455e50 100644
--- a/be/src/format_v2/expr/cast.h
+++ b/be/src/format_v2/expr/cast.h
@@ -53,6 +53,9 @@ public:
std::string debug_string() const override;
uint64_t get_digest(uint64_t seed) const override { return 0; }
const std::string& expr_name() const override { return _expr_name; }
+ // Ordinary CAST can fail for data-dependent input. Localization must
retain the same
+ // full-batch error behavior as VCastExpr instead of hiding errors in
previously rejected rows.
+ bool is_safe_to_execute_on_selected_rows() const override { return false; }
Status clone_node(VExprSPtr* cloned_expr) const override {
DORIS_CHECK(cloned_expr != nullptr);
*cloned_expr = Cast::create_shared(_data_type);
diff --git a/be/src/format_v2/parquet/parquet_profile.cpp
b/be/src/format_v2/parquet/parquet_profile.cpp
index b56a57d419e..9332dac0182 100644
--- a/be/src/format_v2/parquet/parquet_profile.cpp
+++ b/be/src/format_v2/parquet/parquet_profile.cpp
@@ -175,6 +175,8 @@ void ParquetProfile::init(RuntimeProfile* profile) {
TUnit::BYTES,
parquet_profile, 1);
predicate_compaction_count = ADD_CHILD_COUNTER_WITH_LEVEL(profile,
"PredicateCompactionCount",
TUnit::UNIT,
parquet_profile, 1);
+ predicate_alignment_columns = ADD_CHILD_COUNTER_WITH_LEVEL(profile,
"PredicateAlignmentColumns",
+ TUnit::UNIT,
parquet_profile, 1);
fixed_width_predicate_direct_batches = ADD_CHILD_COUNTER_WITH_LEVEL(
profile, "FixedWidthPredicateDirectBatches", TUnit::UNIT,
parquet_profile, 1);
fixed_width_predicate_direct_rows = ADD_CHILD_COUNTER_WITH_LEVEL(
@@ -316,6 +318,7 @@ ParquetScanProfile ParquetProfile::scan_profile() const {
.predicate_compaction_time = predicate_compaction_time,
.predicate_compaction_bytes = predicate_compaction_bytes,
.predicate_compaction_count = predicate_compaction_count,
+ .predicate_alignment_columns = predicate_alignment_columns,
.fixed_width_predicate_direct_batches =
fixed_width_predicate_direct_batches,
.fixed_width_predicate_direct_rows =
fixed_width_predicate_direct_rows,
.dict_filter_rewrite_time = dict_filter_rewrite_time,
diff --git a/be/src/format_v2/parquet/parquet_profile.h
b/be/src/format_v2/parquet/parquet_profile.h
index 170f14b56c5..438d1a9a4b2 100644
--- a/be/src/format_v2/parquet/parquet_profile.h
+++ b/be/src/format_v2/parquet/parquet_profile.h
@@ -88,6 +88,7 @@ struct ParquetScanProfile {
RuntimeProfile::Counter* predicate_compaction_time = nullptr;
RuntimeProfile::Counter* predicate_compaction_bytes = nullptr;
RuntimeProfile::Counter* predicate_compaction_count = nullptr;
+ RuntimeProfile::Counter* predicate_alignment_columns = nullptr;
RuntimeProfile::Counter* fixed_width_predicate_direct_batches = nullptr;
RuntimeProfile::Counter* fixed_width_predicate_direct_rows = nullptr;
RuntimeProfile::Counter* dict_filter_rewrite_time = nullptr; // dictionary
rewrite time (ns)
@@ -203,6 +204,7 @@ struct ParquetProfile {
RuntimeProfile::Counter* predicate_compaction_time = nullptr;
RuntimeProfile::Counter* predicate_compaction_bytes = nullptr;
RuntimeProfile::Counter* predicate_compaction_count = nullptr;
+ RuntimeProfile::Counter* predicate_alignment_columns = nullptr;
RuntimeProfile::Counter* fixed_width_predicate_direct_batches = nullptr;
RuntimeProfile::Counter* fixed_width_predicate_direct_rows = nullptr;
RuntimeProfile::Counter* dict_filter_rewrite_time = nullptr;
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp
b/be/src/format_v2/parquet/parquet_scan.cpp
index b92a3c27054..3730f303254 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -22,6 +22,7 @@
#include <optional>
#include <ranges>
#include <set>
+#include <span>
#include <unordered_set>
#include <utility>
@@ -569,8 +570,8 @@ Status finalize_parquet_row_group_plans(
namespace {
-using DictionaryResidualConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
-using DictionaryResidualConjuncts = std::vector<DictionaryResidualConjunct>;
+using OwnedExpressionConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
+using OwnedExpressionConjuncts = std::vector<OwnedExpressionConjunct>;
void update_counter_if_not_null(RuntimeProfile::Counter* counter, int64_t
value) {
if (counter != nullptr) {
@@ -615,10 +616,9 @@ Status execute_compact_filter_conjuncts(const
VExprContextSPtrs& conjuncts, size
return Status::OK();
}
-Status execute_compact_dictionary_residual_conjuncts(const
DictionaryResidualConjuncts& conjuncts,
- size_t rows, Block*
file_block,
- IColumn::Filter*
compact_filter,
- bool* can_filter_all) {
+Status execute_compact_owned_conjuncts(std::span<const
OwnedExpressionConjunct> conjuncts,
+ size_t rows, Block* file_block,
+ IColumn::Filter* compact_filter, bool*
can_filter_all) {
DORIS_CHECK(compact_filter != nullptr);
DORIS_CHECK(can_filter_all != nullptr);
compact_filter->resize_fill(rows, 1);
@@ -890,6 +890,7 @@ void ParquetScanScheduler::reset() {
_predicate_schedule = {};
_predicate_positions_scratch.clear();
_predicate_indices_by_position_scratch.clear();
+ _materialized_predicate_positions_scratch.clear();
_ordered_predicate_positions_scratch.clear();
_predicate_batch_sequence = 0;
reset_current_row_group();
@@ -964,8 +965,10 @@ const detail::PredicateConjunctSchedule&
ParquetScanScheduler::predicate_conjunc
_predicate_schedule_request = &request;
_predicate_positions_scratch.clear();
_predicate_indices_by_position_scratch.clear();
+ _materialized_predicate_positions_scratch.clear();
_predicate_positions_scratch.reserve(request.predicate_columns.size());
_predicate_indices_by_position_scratch.reserve(request.predicate_columns.size());
+
_materialized_predicate_positions_scratch.reserve(request.predicate_columns.size());
for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
const auto position_it =
request.local_positions.find(request.predicate_columns[idx].column_id());
@@ -978,18 +981,42 @@ const detail::PredicateConjunctSchedule&
ParquetScanScheduler::predicate_conjunc
}
std::vector<format::LocalColumnIndex>
ParquetScanScheduler::adaptive_predicate_prefetch_columns(
- const format::FileScanRequest& request) const {
+ const format::FileScanRequest& request) {
std::vector<size_t> positions;
std::unordered_map<size_t, const format::LocalColumnIndex*>
columns_by_position;
- positions.reserve(request.predicate_columns.size());
columns_by_position.reserve(request.predicate_columns.size());
for (const auto& column : request.predicate_columns) {
const auto position_it =
request.local_positions.find(column.column_id());
DORIS_CHECK(position_it != request.local_positions.end());
const size_t position = position_it->second.value();
- positions.push_back(position);
columns_by_position.emplace(position, &column);
}
+ const auto& schedule = predicate_conjunct_schedule(request);
+ if (!schedule.supports_lazy_materialization) {
+ positions.reserve(request.predicate_columns.size());
+ for (const auto& column : request.predicate_columns) {
+
positions.push_back(request.local_positions.at(column.column_id()).value());
+ }
+ } else if (!schedule.single_column_conjuncts.empty()) {
+ positions.reserve(schedule.single_column_conjuncts.size());
+ for (const auto& column : request.predicate_columns) {
+ const size_t position =
request.local_positions.at(column.column_id()).value();
+ if (schedule.single_column_conjuncts.contains(position)) {
+ // Cold adaptive statistics intentionally preserve request
order; iterating the
+ // hash map here would make the first decoded predicate depend
on bucket layout.
+ positions.push_back(position);
+ }
+ }
+ } else if (!schedule.remaining_stages.empty()) {
+ // Match execution's first reachable stage. Warming columns owned only
by later residuals
+ // would turn lazy decode into eager remote IO before an earlier
conjunct can reject rows.
+ positions = schedule.remaining_stages.front().required_positions;
+ } else {
+ positions.reserve(request.predicate_columns.size());
+ for (const auto& column : request.predicate_columns) {
+
positions.push_back(request.local_positions.at(column.column_id()).value());
+ }
+ }
auto ordered = detail::order_adaptive_predicates(positions,
_predicate_runtime_stats);
ordered = detail::adaptive_prefetch_prefix(ordered,
_predicate_runtime_stats, 0.25);
std::vector<format::LocalColumnIndex> result;
@@ -1214,6 +1241,37 @@ Status
ParquetScanScheduler::flush_pending_non_predicate_skip_rows() {
namespace {
+bool append_residual_stages(const VExprContextSPtr& owner_context, const
VExprSPtr& expression,
+ const std::unordered_set<size_t>&
predicate_block_positions,
+ std::vector<detail::PredicateConjunctStage>*
stages) {
+ DORIS_CHECK(owner_context != nullptr);
+ DORIS_CHECK(expression != nullptr);
+ DORIS_CHECK(stages != nullptr);
+ const auto* compound_predicate = dynamic_cast<const
VCompoundPred*>(expression.get());
+ if (compound_predicate != nullptr && compound_predicate->op() ==
TExprOpcode::COMPOUND_AND) {
+ for (const auto& child : expression->children()) {
+ if (!append_residual_stages(owner_context, child,
predicate_block_positions, stages)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ std::set<int> referenced_positions;
+ expression->collect_slot_column_ids(referenced_positions);
+ auto& stage = stages->emplace_back();
+ stage.owner_context = owner_context;
+ stage.expression = expression;
+ for (const int position : referenced_positions) {
+ if (position < 0 ||
!predicate_block_positions.contains(cast_set<size_t>(position))) {
+ stages->pop_back();
+ return false;
+ }
+ stage.required_positions.push_back(cast_set<size_t>(position));
+ }
+ return true;
+}
+
detail::PredicateConjunctSchedule build_predicate_conjunct_schedule(
const format::FileScanRequest& request) {
std::unordered_set<size_t> predicate_block_positions;
@@ -1235,18 +1293,31 @@ detail::PredicateConjunctSchedule
build_predicate_conjunct_schedule(
// optimization, so any unsafe conjunct disables the per-column
schedule for the batch.
schedule.remaining_conjuncts = request.conjuncts;
schedule.single_column_conjuncts.clear();
+ schedule.remaining_stages.clear();
+ schedule.supports_lazy_materialization = false;
return schedule;
}
std::set<int> referenced_positions;
conjunct->root()->collect_slot_column_ids(referenced_positions);
if (referenced_positions.size() != 1) {
schedule.remaining_conjuncts.push_back(conjunct);
+ if (!append_residual_stages(conjunct, conjunct->root(),
predicate_block_positions,
+ &schedule.remaining_stages)) {
+ schedule.supports_lazy_materialization = false;
+ schedule.remaining_conjuncts = request.conjuncts;
+ schedule.single_column_conjuncts.clear();
+ schedule.remaining_stages.clear();
+ return schedule;
+ }
continue;
}
const auto block_position =
static_cast<size_t>(*referenced_positions.begin());
if (!predicate_block_positions.contains(block_position)) {
- schedule.remaining_conjuncts.push_back(conjunct);
- continue;
+ schedule.supports_lazy_materialization = false;
+ schedule.remaining_conjuncts = request.conjuncts;
+ schedule.single_column_conjuncts.clear();
+ schedule.remaining_stages.clear();
+ return schedule;
}
schedule.single_column_conjuncts[block_position].push_back(conjunct);
}
@@ -1280,7 +1351,7 @@ bool can_evaluate_dictionary_exactly(const VExprSPtr&
expr) {
}
void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context,
const VExprSPtr& expr,
- DictionaryResidualConjuncts*
residual_conjuncts) {
+ OwnedExpressionConjuncts*
residual_conjuncts) {
DORIS_CHECK(owner_context != nullptr);
DORIS_CHECK(expr != nullptr);
DORIS_CHECK(residual_conjuncts != nullptr);
@@ -1304,9 +1375,8 @@ void collect_dictionary_residual_exprs(const
VExprContextSPtr& owner_context, co
residual_conjuncts->emplace_back(owner_context, expr);
}
-DictionaryResidualConjuncts build_dictionary_residual_conjuncts(
- const VExprContextSPtrs& conjuncts) {
- DictionaryResidualConjuncts residual_conjuncts;
+OwnedExpressionConjuncts build_dictionary_residual_conjuncts(const
VExprContextSPtrs& conjuncts) {
+ OwnedExpressionConjuncts residual_conjuncts;
for (const auto& conjunct : conjuncts) {
DORIS_CHECK(conjunct != nullptr);
collect_dictionary_residual_exprs(conjunct, conjunct->root(),
&residual_conjuncts);
@@ -1426,7 +1496,7 @@ Status
ParquetScanScheduler::prepare_current_dictionary_filters(
// VCompoundPred intentionally evaluates only dictionary-capable
children, so residual
// predicates still run later on surviving rows.
IColumn::Filter dictionary_filter;
- DictionaryResidualConjuncts residual_conjuncts;
+ OwnedExpressionConjuncts residual_conjuncts;
{
SCOPED_TIMER(_scan_profile.dict_filter_build_time);
dictionary_filter = build_dictionary_entry_filter(
@@ -1471,14 +1541,18 @@ Status
ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
remember_residual_positions(schedule.remaining_conjuncts);
remember_residual_positions(request.delete_conjuncts);
const size_t predicate_batch_sequence = _predicate_batch_sequence++;
- const bool can_read_predicate_columns_round_by_round =
- !schedule.single_column_conjuncts.empty();
+ const bool can_read_predicate_columns_round_by_round =
schedule.supports_lazy_materialization;
auto& read_column_positions = _read_column_positions_scratch;
read_column_positions.clear();
read_column_positions.reserve(request.predicate_columns.size());
+ auto& materialized_positions = _materialized_predicate_positions_scratch;
+ materialized_positions.clear();
for (auto& rows : _predicate_column_selection_scratch |
std::views::values) {
rows.clear();
}
+ // A generation becomes dirty only when filtering changes SelectionVector.
Columns read after
+ // an all-pass stage already share its coordinates, so rewalking every
prior mapping is wasted.
+ bool predicate_columns_need_alignment = false;
auto remember_column_selection = [&](uint32_t position) {
auto& rows = _predicate_column_selection_scratch[position];
@@ -1493,6 +1567,8 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
auto compact_predicate_columns = [&](bool discard_predicate_only_payload)
-> Status {
bool compacted = false;
int64_t compacted_bytes = 0;
+ update_counter_if_not_null(_scan_profile.predicate_alignment_columns,
+
cast_set<int64_t>(read_column_positions.size()));
for (const uint32_t position : read_column_positions) {
auto& source_rows = _predicate_column_selection_scratch[position];
const auto& old_column =
file_block->get_by_position(position).column;
@@ -1705,6 +1781,7 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
static_cast<int64_t>(new_selected_rows);
}
if (new_selected_rows != selected_rows_before) {
+ predicate_columns_need_alignment = true;
*selected_rows = can_filter_all
? 0
:
apply_compact_filter_to_selection(compact_filter, selection,
@@ -1713,16 +1790,16 @@ Status
ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
return Status::OK();
};
- auto execute_scheduled_dictionary_residual_conjuncts =
- [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+ auto execute_scheduled_owned_conjuncts =
+ [&](std::span<const OwnedExpressionConjunct> conjuncts) -> Status {
if (conjuncts.empty() || *selected_rows == 0) {
return Status::OK();
}
const uint16_t selected_rows_before = *selected_rows;
IColumn::Filter compact_filter;
bool can_filter_all = false;
- RETURN_IF_ERROR(execute_compact_dictionary_residual_conjuncts(
- conjuncts, selected_rows_before, file_block, &compact_filter,
&can_filter_all));
+ RETURN_IF_ERROR(execute_compact_owned_conjuncts(conjuncts,
selected_rows_before, file_block,
+ &compact_filter,
&can_filter_all));
if (can_filter_all) {
compact_filter.resize_fill(selected_rows_before, 0);
}
@@ -1732,6 +1809,7 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
static_cast<int64_t>(new_selected_rows);
}
if (new_selected_rows != selected_rows_before) {
+ predicate_columns_need_alignment = true;
*selected_rows = can_filter_all
? 0
:
apply_compact_filter_to_selection(compact_filter, selection,
@@ -1749,13 +1827,13 @@ Status
ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
return execute_scheduled_conjuncts(conjuncts);
};
- auto execute_scheduled_dictionary_residual_conjuncts_with_profile =
- [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+ auto execute_scheduled_owned_conjuncts_with_profile =
+ [&](std::span<const OwnedExpressionConjunct> conjuncts) -> Status {
if (_scan_profile.predicate_filter_time == nullptr) {
- return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+ return execute_scheduled_owned_conjuncts(conjuncts);
}
SCOPED_TIMER(_scan_profile.predicate_filter_time);
- return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+ return execute_scheduled_owned_conjuncts(conjuncts);
};
auto execute_scheduled_delete_conjuncts = [&]() -> Status {
@@ -1772,6 +1850,7 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
compact_filter.resize_fill(selected_rows_before, 0);
}
if (can_filter_all || count_selected_rows(compact_filter) !=
selected_rows_before) {
+ predicate_columns_need_alignment = true;
*selected_rows = can_filter_all
? 0
:
apply_compact_filter_to_selection(compact_filter, selection,
@@ -1789,6 +1868,7 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
RETURN_IF_ERROR(read_predicate_column(column_reader.get(),
position_it->second.value(),
fid, nullptr,
&used_dictionary_filter,
&used_fixed_width_filter));
+ materialized_positions.insert(position_it->second.value());
}
return Status::OK();
};
@@ -1808,8 +1888,18 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
// Single-column conjuncts can be evaluated immediately after their
column is read. Once
// selection shrinks, later predicate columns use
ParquetColumnReader::select() so the
// reader skips rows already rejected by earlier predicates instead of
materializing them.
+ _ordered_predicate_positions_scratch.clear();
+
_ordered_predicate_positions_scratch.reserve(schedule.single_column_conjuncts.size());
+ for (const auto& column : request.predicate_columns) {
+ const size_t position =
request.local_positions.at(column.column_id()).value();
+ if (schedule.single_column_conjuncts.contains(position)) {
+ // The request order is the stable cold-start policy until
measured costs can
+ // reorder predicates; unordered-map iteration can defeat an
early selective filter.
+ _ordered_predicate_positions_scratch.push_back(position);
+ }
+ }
_ordered_predicate_positions_scratch =
detail::order_adaptive_predicates(
- _predicate_positions_scratch, _predicate_runtime_stats);
+ _ordered_predicate_positions_scratch,
_predicate_runtime_stats);
const auto& ordered_positions = _ordered_predicate_positions_scratch;
for (size_t order_idx = 0; order_idx < ordered_positions.size();
++order_idx) {
const size_t position = ordered_positions[order_idx];
@@ -1835,16 +1925,20 @@ Status
ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(),
block_position, fid,
column_conjuncts,
&used_dictionary_filter,
&used_fixed_width_filter));
+ materialized_positions.insert(block_position);
if (*selected_rows != 0 && conjunct_it !=
schedule.single_column_conjuncts.end()) {
if (used_dictionary_filter) {
const auto residual_it =
_current_dictionary_residual_conjuncts.find(fid);
DORIS_CHECK(residual_it !=
_current_dictionary_residual_conjuncts.end());
-
RETURN_IF_ERROR(execute_scheduled_dictionary_residual_conjuncts_with_profile(
- residual_it->second));
+ RETURN_IF_ERROR(
+
execute_scheduled_owned_conjuncts_with_profile(residual_it->second));
} else if (!used_fixed_width_filter) {
RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));
}
}
+ if (*selected_rows != rows_before) {
+ predicate_columns_need_alignment = true;
+ }
if (sample) {
const double cost_per_row =
static_cast<double>(MonotonicNanos() - start_ns) /
std::max<uint16_t>(rows_before, 1);
@@ -1866,39 +1960,103 @@ Status
ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
if (*selected_rows != 0) {
continue;
}
- for (size_t remaining_order_idx = order_idx + 1;
- remaining_order_idx < ordered_positions.size();
++remaining_order_idx) {
- const size_t remaining_idx =
_predicate_indices_by_position_scratch.at(
- ordered_positions[remaining_order_idx]);
- const auto remaining_fid =
request.predicate_columns[remaining_idx].column_id();
- auto remaining_reader_it =
_current_predicate_columns.find(remaining_fid);
- DORIS_CHECK(remaining_reader_it !=
_current_predicate_columns.end());
- RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
- }
return Status::OK();
}
return Status::OK();
};
+ auto materialize_predicate_positions = [&](const std::vector<size_t>&
positions) -> Status {
+ for (const size_t position : positions) {
+ if (materialized_positions.contains(position)) {
+ continue;
+ }
+ const auto index_it =
_predicate_indices_by_position_scratch.find(position);
+ DORIS_CHECK(index_it !=
_predicate_indices_by_position_scratch.end());
+ const auto fid =
request.predicate_columns[index_it->second].column_id();
+ const auto reader_it = _current_predicate_columns.find(fid);
+ DORIS_CHECK(reader_it != _current_predicate_columns.end());
+ bool used_dictionary_filter = false;
+ bool used_fixed_width_filter = false;
+ RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(),
position, fid, nullptr,
+ &used_dictionary_filter,
+ &used_fixed_width_filter));
+ materialized_positions.insert(position);
+ }
+ return Status::OK();
+ };
+
+ auto skip_unmaterialized_predicate_columns = [&]() -> Status {
+ for (const auto& col : request.predicate_columns) {
+ const auto position_it =
request.local_positions.find(col.column_id());
+ DORIS_CHECK(position_it != request.local_positions.end());
+ if (materialized_positions.contains(position_it->second.value())) {
+ continue;
+ }
+ const auto reader_it =
_current_predicate_columns.find(col.column_id());
+ DORIS_CHECK(reader_it != _current_predicate_columns.end());
+ RETURN_IF_ERROR(reader_it->second->skip(batch_rows));
+ }
+ // Every skipped column has an empty payload in the block. Suppress
the caller's
+ // batch-coordinate filter because there is no materialized
batch-sized column left.
+ *predicate_columns_filtered = true;
+ return Status::OK();
+ };
+
auto compact_predicate_columns_with_profile =
[&](bool discard_predicate_only_payload) -> Status {
+ if (!discard_predicate_only_payload &&
!predicate_columns_need_alignment) {
+ return Status::OK();
+ }
const int64_t start_ns = MonotonicNanos();
auto status =
compact_predicate_columns(discard_predicate_only_payload);
update_counter_if_not_null(_scan_profile.predicate_compaction_time,
MonotonicNanos() - start_ns);
+ if (status.ok()) {
+ predicate_columns_need_alignment = false;
+ }
return status;
};
RETURN_IF_ERROR(read_round_by_round());
- // Single-column expressions only touch the just-read column, so earlier
columns can retain
- // their own row mappings. Compact only when a later expression needs a
shared coordinate
- // space; otherwise the final boundary can discard hidden predicate
payloads without scanning
- // them again.
- if (!schedule.remaining_conjuncts.empty()) {
+ if (*selected_rows == 0) {
+ RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
+ return compact_predicate_columns_with_profile(true);
+ }
+
+ // Complex residuals keep their original conjunct order. Materialize only
the columns needed
+ // by the next reachable expression, then compact previously read columns
into the same row
+ // space before evaluating it. This is the scanner-side equivalent of
expression-triggered
+ // lazy columns: a conjunct that rejects the batch prevents later-only
columns from decoding.
+ for (const auto& stage : schedule.remaining_stages) {
+
RETURN_IF_ERROR(materialize_predicate_positions(stage.required_positions));
RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
+ const OwnedExpressionConjunct stage_conjunct {stage.owner_context,
stage.expression};
+ RETURN_IF_ERROR(execute_scheduled_owned_conjuncts_with_profile(
+ std::span<const OwnedExpressionConjunct>(&stage_conjunct, 1)));
+ if (*selected_rows == 0) {
+ RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
+ return compact_predicate_columns_with_profile(true);
+ }
}
-
RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(schedule.remaining_conjuncts));
+
if (!request.delete_conjuncts.empty()) {
+ std::set<int> delete_positions;
+ for (const auto& conjunct : request.delete_conjuncts) {
+ DORIS_CHECK(conjunct != nullptr && conjunct->root() != nullptr);
+ conjunct->root()->collect_slot_column_ids(delete_positions);
+ }
+ std::vector<size_t> required_delete_positions;
+ required_delete_positions.reserve(delete_positions.size());
+ for (const int position : delete_positions) {
+ DORIS_CHECK(position >= 0);
+ required_delete_positions.push_back(cast_set<size_t>(position));
+ }
+ if (required_delete_positions.empty() &&
!_predicate_positions_scratch.empty()) {
+ // An all-literal equality-delete predicate has no slot
dependency, but its hidden
+ // row-count carrier must still be materialized so the result
matches selected_rows.
+
required_delete_positions.push_back(_predicate_positions_scratch.front());
+ }
+
RETURN_IF_ERROR(materialize_predicate_positions(required_delete_positions));
RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
}
if (_scan_profile.predicate_filter_time == nullptr) {
@@ -1907,6 +2065,11 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
SCOPED_TIMER(_scan_profile.predicate_filter_time);
RETURN_IF_ERROR(execute_scheduled_delete_conjuncts());
}
+ if (*selected_rows == 0) {
+ RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
+ return compact_predicate_columns_with_profile(true);
+ }
+
RETURN_IF_ERROR(materialize_predicate_positions(_predicate_positions_scratch));
return compact_predicate_columns_with_profile(true);
}
diff --git a/be/src/format_v2/parquet/parquet_scan.h
b/be/src/format_v2/parquet/parquet_scan.h
index ac883cbbcbe..f0e202d9942 100644
--- a/be/src/format_v2/parquet/parquet_scan.h
+++ b/be/src/format_v2/parquet/parquet_scan.h
@@ -58,9 +58,17 @@ struct ParquetScanRange;
class NativeParquetMetadata;
namespace detail {
+struct PredicateConjunctStage {
+ VExprContextSPtr owner_context;
+ VExprSPtr expression;
+ std::vector<size_t> required_positions;
+};
+
struct PredicateConjunctSchedule {
std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
VExprContextSPtrs remaining_conjuncts;
+ std::vector<PredicateConjunctStage> remaining_stages;
+ bool supports_lazy_materialization = true;
};
struct AdaptivePredicateStats {
@@ -208,7 +216,7 @@ private:
const detail::PredicateConjunctSchedule& predicate_conjunct_schedule(
const format::FileScanRequest& request);
std::vector<format::LocalColumnIndex> adaptive_predicate_prefetch_columns(
- const format::FileScanRequest& request) const;
+ const format::FileScanRequest& request);
Status open_next_row_group(ParquetFileContext& file_context,
const
std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
@@ -305,6 +313,7 @@ private:
detail::PredicateConjunctSchedule _predicate_schedule;
std::vector<size_t> _predicate_positions_scratch;
std::unordered_map<size_t, size_t> _predicate_indices_by_position_scratch;
+ std::unordered_set<size_t> _materialized_predicate_positions_scratch;
std::vector<size_t> _ordered_predicate_positions_scratch;
std::unordered_map<uint32_t, std::vector<SelectionVector::Index>>
_predicate_column_selection_scratch;
diff --git a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
index 490a0bde204..45c1fe9dd44 100644
--- a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
+++ b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
@@ -22,6 +22,7 @@
#include <algorithm>
#include <array>
#include <set>
+#include <string>
#include <tuple>
namespace doris::parquet_benchmark {
@@ -86,7 +87,8 @@ TEST(ParquetBenchmarkScenariosTest,
ReaderMatrixCoversOperationsEncodingsAndSche
const auto scenarios = reader_scenarios();
for (const auto operation :
{ReaderOperation::OPEN_TO_FIRST_BLOCK, ReaderOperation::FULL_SCAN,
- ReaderOperation::PREDICATE_SCAN, ReaderOperation::LIMIT_1,
ReaderOperation::LIMIT_1000}) {
+ ReaderOperation::PREDICATE_SCAN,
ReaderOperation::COMPLEX_RESIDUAL_SCAN,
+ ReaderOperation::LIMIT_1, ReaderOperation::LIMIT_1000}) {
EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const ReaderScenario&
scenario) {
return scenario.operation == operation;
}));
@@ -109,6 +111,26 @@ TEST(ParquetBenchmarkScenariosTest,
ReaderMatrixCoversOperationsEncodingsAndSche
}
}
+TEST(ParquetBenchmarkScenariosTest,
ReaderMatrixHasExactUniqueRegistrationNames) {
+ const auto scenarios = reader_scenarios();
+ EXPECT_EQ(scenarios.size(), 152);
+
+ std::set<std::string> names;
+ for (const auto& scenario : scenarios) {
+ names.insert(reader_scenario_name(scenario));
+ }
+ EXPECT_EQ(names.size(), scenarios.size());
+}
+
+TEST(ParquetBenchmarkScenariosTest,
ReaderMatrixCoversComplexResidualLazyMaterialization) {
+ const auto scenarios = reader_scenarios();
+ EXPECT_TRUE(std::ranges::any_of(scenarios, [](const ReaderScenario&
scenario) {
+ return scenario.operation == ReaderOperation::COMPLEX_RESIDUAL_SCAN &&
+ scenario.encoding == Encoding::PLAIN &&
scenario.selectivity_percent == 10 &&
+ scenario.schema_width == 32;
+ }));
+}
+
TEST(ParquetBenchmarkScenariosTest, ReaderMatrixCoversFixedWidthRawFilterAxes)
{
const auto scenarios = reader_scenarios();
for (const auto encoding : {Encoding::BYTE_STREAM_SPLIT,
Encoding::DELTA_BINARY_PACKED}) {
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index 9bdb0c7562f..a4e5abb91a4 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -46,11 +46,13 @@
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
#include "core/field.h"
+#include "exprs/vcompound_pred.h"
#include "exprs/vectorized_fn_call.h"
#include "exprs/vexpr.h"
#include "exprs/vexpr_context.h"
#include "exprs/vliteral.h"
#include "exprs/vslot_ref.h"
+#include "format_v2/expr/cast.h"
#include "format_v2/expr/delete_predicate.h"
#include "format_v2/file_reader.h"
#include "format_v2/parquet/parquet_column_schema.h"
@@ -232,6 +234,8 @@ public:
const std::string& expr_name() const override { return _expr_name; }
+ bool is_constant() const override { return false; }
+
Status execute_column_impl(VExprContext*, const Block* block, const
Selector* selector,
size_t count, ColumnPtr& result_column) const
override {
DORIS_CHECK(block != nullptr);
@@ -470,6 +474,59 @@ VExprContextSPtr create_int32_pair_sum_conjunct(int
left_column_id, int right_co
std::make_shared<Int32PairSumExpr>(left_column_id,
right_column_id, upper_bound));
}
+VExprContextSPtr create_and_conjunct(VExprSPtr left, VExprSPtr right) {
+ const auto result_type = left->data_type()->is_nullable() ||
right->data_type()->is_nullable()
+ ?
make_nullable(std::make_shared<DataTypeUInt8>())
+ : std::make_shared<DataTypeUInt8>();
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+ node.__set_opcode(TExprOpcode::COMPOUND_AND);
+ node.__set_type(result_type->to_thrift());
+ node.__set_num_children(2);
+ node.__set_is_nullable(result_type->is_nullable());
+ auto compound = VCompoundPred::create_shared(node);
+ compound->add_child(std::move(left));
+ compound->add_child(std::move(right));
+ return VExprContext::create_shared(std::move(compound));
+}
+
+VExprSPtr create_binary_predicate(const std::string& function_name,
TExprOpcode::type opcode,
+ VExprSPtr left, VExprSPtr right) {
+ const auto result_type = left->data_type()->is_nullable() ||
right->data_type()->is_nullable()
+ ?
make_nullable(std::make_shared<DataTypeUInt8>())
+ : std::make_shared<DataTypeUInt8>();
+ TFunctionName name;
+ name.__set_function_name(function_name);
+ TFunction function;
+ function.__set_name(name);
+ function.__set_binary_type(TFunctionBinaryType::BUILTIN);
+ function.__set_arg_types({left->data_type()->to_thrift(),
right->data_type()->to_thrift()});
+ function.__set_ret_type(result_type->to_thrift());
+ function.__set_has_var_args(false);
+ TExprNode node;
+ node.__set_node_type(TExprNodeType::BINARY_PRED);
+ node.__set_opcode(opcode);
+ node.__set_type(result_type->to_thrift());
+ node.__set_fn(function);
+ node.__set_num_children(2);
+ node.__set_is_nullable(result_type->is_nullable());
+ auto predicate = VectorizedFnCall::create_shared(node);
+ predicate->add_child(std::move(left));
+ predicate->add_child(std::move(right));
+ return predicate;
+}
+
+VExprSPtr create_int32_slot_comparison(const std::string& function_name,
TExprOpcode::type opcode,
+ int left_column_id, int right_column_id,
+ const DataTypePtr& type) {
+ return create_binary_predicate(
+ function_name, opcode,
+ VSlotRef::create_shared(left_column_id, left_column_id, -1, type,
+ "c" + std::to_string(left_column_id)),
+ VSlotRef::create_shared(right_column_id, right_column_id, -1, type,
+ "c" + std::to_string(right_column_id)));
+}
+
VExprContextSPtr create_int32_direct_greater_conjunct(int column_id, int32_t
lower_bound) {
return VExprContext::create_shared(
std::make_shared<Int32DirectGreaterExpr>(column_id, lower_bound));
@@ -635,6 +692,41 @@ void write_int_pair_parquet_file(const std::string&
file_path, int64_t row_group
write_table(file_path, table, row_group_size, false, false,
enable_statistics, encoding);
}
+void write_int_triple_parquet_file(const std::string& file_path) {
+ auto schema = arrow::schema({
+ arrow::field("left", arrow::int32(), false),
+ arrow::field("middle", arrow::int32(), false),
+ arrow::field("right", arrow::int32(), false),
+ });
+ auto table = arrow::Table::Make(schema, {build_int32_array({1, 2, 3, 4, 5,
6}),
+ build_int32_array({10, 20, 30, 0,
0, 0}),
+ build_int32_array({100, 200, 300,
400, 500, 600})});
+ write_table(file_path, table, 6, false, false, false);
+}
+
+void write_int_columns_parquet_file(const std::string& file_path, int
column_count) {
+ std::vector<std::shared_ptr<arrow::Field>> fields;
+ std::vector<std::shared_ptr<arrow::Array>> columns;
+ fields.reserve(column_count);
+ columns.reserve(column_count);
+ for (int column = 0; column < column_count; ++column) {
+ fields.push_back(arrow::field("c" + std::to_string(column),
arrow::int32(), false));
+ columns.push_back(build_int32_array({1, 2, 3, 4, 5, 6}));
+ }
+ write_table(file_path,
arrow::Table::Make(arrow::schema(std::move(fields)), std::move(columns)),
+ 6, false, false, false);
+}
+
+void write_int_pair_and_string_parquet_file(const std::string& file_path) {
+ auto schema = arrow::schema({arrow::field("left", arrow::int32(), false),
+ arrow::field("right", arrow::int32(), false),
+ arrow::field("text", arrow::utf8(), false)});
+ auto table =
+ arrow::Table::Make(schema, {build_int32_array({1, 2, 3}),
build_int32_array({0, 3, 4}),
+ build_string_array({"bad", "2",
"3"})});
+ write_table(file_path, table, 3, false, false, false);
+}
+
void write_uint32_pair_parquet_file(const std::string& file_path) {
auto schema = arrow::schema({arrow::field("id", arrow::uint32(), false),
arrow::field("score", arrow::int32(),
false)});
@@ -1349,8 +1441,7 @@ TEST_F(ParquetScanTest,
PredicateOnlyGlobalRowIdKeepsSignedFileLocalId) {
Block block = build_file_block(schema);
size_t rows = 0;
bool eof = false;
- const auto status = reader->get_block(&block, &rows, &eof);
- ASSERT_TRUE(status.ok()) << status;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
EXPECT_EQ(rows, 6);
EXPECT_EQ(int32_data_column(*block.get_by_position(0).column).get_data(),
(ColumnInt32::Container {1, 2, 3, 4, 5, 6}));
@@ -1372,7 +1463,8 @@ TEST_F(ParquetScanTest,
EmptyScanPlanReturnsEofWithoutReadingColumns) {
Block block = build_file_block(schema);
size_t rows = 0;
bool eof = false;
- ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ const auto status = reader->get_block(&block, &rows, &eof);
+ ASSERT_TRUE(status.ok()) << status;
EXPECT_EQ(rows, 0);
EXPECT_TRUE(eof);
}
@@ -1485,6 +1577,149 @@ TEST_F(ParquetScanTest,
PredicateColumnsSkipUnreadColumnsWhenFirstPredicateFilte
EXPECT_EQ(counter_value(profile, "ReaderSkipRows"), 6);
}
+TEST_F(ParquetScanTest,
ComplexResidualSkipsColumnsAfterEarlierAndChildFiltersAll) {
+ write_int_triple_parquet_file(_file_path);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(1)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(2)).ok());
+ auto context = create_and_conjunct(
+ create_int32_slot_comparison("eq", TExprOpcode::EQ, 0, 1,
schema[0].type),
+ create_int32_slot_comparison("lt", TExprOpcode::LT, 1, 2,
schema[1].type));
+ ASSERT_TRUE(context->prepare(&state, RowDescriptor()).ok());
+ ASSERT_TRUE(context->open(&state).ok());
+ request->conjuncts.push_back(context);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ EXPECT_EQ(rows, 0);
+ EXPECT_EQ(counter_value(profile, "ReaderReadRows"), 12);
+ EXPECT_EQ(counter_value(profile, "ReaderSelectRows"), 0);
+ EXPECT_EQ(counter_value(profile, "ReaderSkipRows"), 6);
+ context->close();
+}
+
+TEST_F(ParquetScanTest, ComplexResidualSelectsLaterColumnsForSurvivingRows) {
+ write_int_triple_parquet_file(_file_path);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(1)).ok());
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(2)).ok());
+ auto context = create_and_conjunct(
+ create_int32_slot_comparison("gt", TExprOpcode::GT, 1, 0,
schema[1].type),
+ create_int32_slot_comparison("lt", TExprOpcode::LT, 1, 2,
schema[1].type));
+ ASSERT_TRUE(context->prepare(&state, RowDescriptor()).ok());
+ ASSERT_TRUE(context->open(&state).ok());
+ request->conjuncts.push_back(context);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ bool eof = false;
+ ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+ ASSERT_EQ(rows, 3);
+ EXPECT_EQ(int32_data_column(*block.get_by_position(0).column).get_data(),
+ (ColumnInt32::Container {1, 2, 3}));
+ EXPECT_EQ(int32_data_column(*block.get_by_position(2).column).get_data(),
+ (ColumnInt32::Container {100, 200, 300}));
+ EXPECT_EQ(counter_value(profile, "ReaderReadRows"), 15);
+ EXPECT_EQ(counter_value(profile, "ReaderSelectRows"), 3);
+ EXPECT_EQ(counter_value(profile, "ReaderSkipRows"), 3);
+ context->close();
+}
+
+TEST_F(ParquetScanTest, AllPassResidualChainAlignsEachColumnOnce) {
+ write_int_columns_parquet_file(_file_path, 6);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ RuntimeState state {TQueryOptions(), TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+ for (int column = 0; column < 6; ++column) {
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(column)).ok());
+ }
+ for (int column = 0; column < 6; column += 2) {
+ auto context =
VExprContext::create_shared(create_int32_slot_comparison(
+ "eq", TExprOpcode::EQ, column, column + 1,
schema[column].type));
+ ASSERT_TRUE(context->prepare(&state, RowDescriptor()).ok());
+ ASSERT_TRUE(context->open(&state).ok());
+ request->conjuncts.push_back(std::move(context));
+ }
+ ASSERT_TRUE(reader->open(request).ok());
+
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ ASSERT_TRUE(status.ok()) << status;
+ ASSERT_EQ(rows, 6);
+ auto* alignment_columns = profile.get_counter("PredicateAlignmentColumns");
+ ASSERT_NE(alignment_columns, nullptr);
+ EXPECT_EQ(alignment_columns->value(), 6);
+}
+
+TEST_F(ParquetScanTest, LocalizedStrictCastPreservesRejectedRowError) {
+ write_int_pair_and_string_parquet_file(_file_path);
+ RuntimeProfile profile("profile");
+ auto reader = create_reader(0, -1, &profile);
+ TQueryOptions query_options;
+ query_options.__set_enable_strict_cast(true);
+ RuntimeState state {query_options, TQueryGlobals()};
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<format::ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<format::FileScanRequest>();
+ format::FileScanRequestBuilder request_builder(request.get());
+ for (int column = 0; column < 3; ++column) {
+
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(column)).ok());
+ }
+
+ auto cast =
format::Cast::create_shared(make_nullable(std::make_shared<DataTypeInt32>()));
+ cast->add_child(VSlotRef::create_shared(2, 2, -1, schema[2].type, "text"));
+ auto first = create_int32_slot_comparison("lt", TExprOpcode::LT, 0, 1,
schema[0].type);
+ auto second =
+ create_binary_predicate("eq", TExprOpcode::EQ, std::move(cast),
+
VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
+
Field::create_field<TYPE_INT>(2)));
+ auto context = create_and_conjunct(std::move(first), std::move(second));
+ EXPECT_FALSE(context->root()->is_safe_to_execute_on_selected_rows());
+ ASSERT_TRUE(context->prepare(&state, RowDescriptor()).ok());
+ ASSERT_TRUE(context->open(&state).ok());
+ request->conjuncts.push_back(context);
+ ASSERT_TRUE(reader->open(request).ok());
+
+ Block block = build_file_block(schema);
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ EXPECT_FALSE(status.ok());
+ context->close();
+}
+
TEST_F(ParquetScanTest, PredicateOnlyColumnDropsPayloadAfterFiltering) {
write_int_pair_parquet_file(_file_path, 6, false);
RuntimeProfile profile("profile");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]