github-actions[bot] commented on code in PR #65369:
URL: https://github.com/apache/doris/pull/65369#discussion_r3549581323


##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -597,42 +780,500 @@ Status 
ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
     return Status::OK();
 }
 
+namespace {
+
+struct PredicateConjunctSchedule {
+    std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
+    VExprContextSPtrs remaining_conjuncts;
+};
+
+PredicateConjunctSchedule build_predicate_conjunct_schedule(
+        const format::FileScanRequest& request) {
+    std::unordered_set<size_t> predicate_block_positions;
+    predicate_block_positions.reserve(request.predicate_columns.size());
+    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());
+        predicate_block_positions.insert(position_it->second.value());
+    }
+
+    PredicateConjunctSchedule schedule;
+    for (const auto& conjunct : request.conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        DORIS_CHECK(conjunct->root() != nullptr);
+        if (!conjunct->root()->is_deterministic()) {
+            // Round-by-round filtering can compact later predicate columns 
before evaluating
+            // remaining expressions. Stateful functions such as random(1) 
must see the same full
+            // batch they saw before this optimization, so any 
non-deterministic conjunct disables
+            // the per-column schedule for the whole batch.
+            schedule.remaining_conjuncts = request.conjuncts;
+            schedule.single_column_conjuncts.clear();
+            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);
+            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.single_column_conjuncts[block_position].push_back(conjunct);
+    }
+    return schedule;
+}
+
+bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
+    if (conjuncts.empty()) {
+        return false;
+    }
+    return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
+        return conjunct != nullptr && conjunct->root() != nullptr &&
+               conjunct->root()->can_evaluate_dictionary_filter();
+    });
+}
+
+void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context, 
const VExprSPtr& expr,
+                                       DictionaryResidualConjuncts* 
residual_conjuncts) {
+    DORIS_CHECK(owner_context != nullptr);
+    DORIS_CHECK(expr != nullptr);
+    DORIS_CHECK(residual_conjuncts != nullptr);
+
+    // The dictionary interface is exact for a single dictionary-aware 
predicate and for OR only
+    // when every child is dictionary-aware. AND is different: VCompoundPred 
evaluates only the
+    // dictionary-aware children as a prefilter. Split AND recursively so the 
already-applied
+    // dictionary children are not executed again on materialized rows, while 
non-dictionary
+    // residual children still keep the original row-level semantics.
+    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
+    if (compound_pred != nullptr && compound_pred->op() == 
TExprOpcode::COMPOUND_AND) {
+        for (const auto& child : expr->children()) {
+            collect_dictionary_residual_exprs(owner_context, child, 
residual_conjuncts);
+        }
+        return;
+    }
+
+    if (!expr->can_evaluate_dictionary_filter()) {

Review Comment:
   This still drops residual work for nested compound predicates. 
`collect_dictionary_residual_exprs()` only descends through `AND`; for any 
other node it assumes `can_evaluate_dictionary_filter()` means the whole 
expression is dictionary-equivalent. But `VCompoundPred` makes `OR` return true 
when each child can dictionary-filter, while an `AND` child returns true when 
only one child can dictionary-filter. A predicate like `(value = 'az') OR 
(value = 'za' AND length(value) = 1)` therefore has a dictionary-evaluable 
root: the dictionary bitmap keeps both `az` and `za`, and no residual is 
recorded for `length(value) = 1`. When `read_round_by_round()` sees 
`used_dictionary_filter`, it runs that empty residual list, so `za` rows 
survive even though the original row predicate is false. Please treat OR as 
dictionary-exact only when every descendant is fully dictionary-equivalent, or 
keep the whole compound expression as a row-level residual when any branch is 
only a prefilter.
   



##########
be/test/format_v2/parquet/parquet_reader_test.cpp:
##########
@@ -1637,6 +1801,183 @@ TEST_F(NewParquetReaderTest, 
PredicateFiltersRowGroupsByDictionary) {
     EXPECT_EQ(values, std::vector<std::string>({"lm"}));
 }
 
+TEST_F(NewParquetReaderTest, DictionaryPredicateFiltersRowsInsideRowGroup) {
+    write_single_row_group_dictionary_filter_parquet_file(_file_path);
+    auto parquet_file_reader = 
::parquet::ParquetFileReader::OpenFile(_file_path, false);
+    ASSERT_EQ(parquet_file_reader->metadata()->num_row_groups(), 1);
+    auto row_group = parquet_file_reader->metadata()->RowGroup(0);
+    ASSERT_NE(row_group, nullptr);
+    ASSERT_TRUE(row_group->ColumnChunk(1)->has_dictionary_page());
+
+    RuntimeProfile profile("new_parquet_reader_dictionary_filter_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>();
+    request->predicate_columns = {field_projection(1)};
+    request->non_predicate_columns = {field_projection(0)};
+    request->conjuncts.push_back(create_string_in_conjunct(1, {"az", "za"}));
+    use_schema_order_positions(request.get(), schema);
+    ASSERT_TRUE(reader->open(request).ok());
+
+    std::vector<int32_t> ids;
+    std::vector<std::string> values;
+    bool eof = false;
+    while (!eof) {
+        Block block = build_file_block(schema);
+        size_t rows = 0;
+        ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+        if (rows == 0) {
+            continue;
+        }
+        const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+        const auto& value_column = nullable_nested_column<ColumnString>(block, 
1);
+        for (size_t row = 0; row < rows; ++row) {
+            ids.push_back(id_column.get_element(row));
+            values.push_back(value_column.get_data_at(row).to_string());
+        }
+    }
+
+    EXPECT_EQ(ids, std::vector<int32_t>({2, 5}));
+    EXPECT_EQ(values, std::vector<std::string>({"az", "za"}));
+    EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 4);
+    EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+    EXPECT_EQ(profile.get_counter("DictFilterCandidateColumns")->value(), 1);
+    EXPECT_EQ(profile.get_counter("DictFilterColumns")->value(), 1);
+    EXPECT_EQ(profile.get_counter("DictFilterUnsupportedColumns")->value(), 0);
+    EXPECT_EQ(profile.get_counter("DictFilterReadFailures")->value(), 0);
+    ASSERT_NE(profile.get_counter("DictFilterExprRewriteTime"), nullptr);
+    ASSERT_NE(profile.get_counter("DictFilterReadDictTime"), nullptr);
+    ASSERT_NE(profile.get_counter("DictFilterBuildTime"), nullptr);
+    EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 2);
+    EXPECT_GE(profile.get_counter("ReaderSelectRows")->value(), 8);
+}
+
+TEST_F(NewParquetReaderTest, DictionaryPredicateWorksWithoutRuntimeProfile) {
+    write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+    auto reader = create_reader();
+    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>();
+    request->predicate_columns = {field_projection(1)};
+    request->non_predicate_columns = {field_projection(0)};
+    request->conjuncts.push_back(create_string_in_conjunct(1, {"az", "za"}));
+    use_schema_order_positions(request.get(), schema);
+    ASSERT_TRUE(reader->open(request).ok());
+
+    std::vector<int32_t> ids;
+    std::vector<std::string> values;
+    bool eof = false;
+    while (!eof) {
+        Block block = build_file_block(schema);
+        size_t rows = 0;
+        ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+        if (rows == 0) {
+            continue;
+        }
+        const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+        const auto& value_column = nullable_nested_column<ColumnString>(block, 
1);
+        for (size_t row = 0; row < rows; ++row) {
+            ids.push_back(id_column.get_element(row));
+            values.push_back(value_column.get_data_at(row).to_string());
+        }
+    }
+
+    EXPECT_EQ(ids, std::vector<int32_t>({2, 5}));
+    EXPECT_EQ(values, std::vector<std::string>({"az", "za"}));
+}
+
+TEST_F(NewParquetReaderTest, 
DictionaryPredicateSkipsRemainingPredicateColumnsWhenEmpty) {
+    write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+    RuntimeProfile 
profile("new_parquet_reader_dictionary_filter_empty_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>();
+    request->predicate_columns = {field_projection(1), field_projection(0)};
+    request->conjuncts.push_back(create_string_in_conjunct(1, 
{"not_present"}));
+    request->conjuncts.push_back(create_int32_greater_than_conjunct(0, 0));
+    use_schema_order_positions(request.get(), schema);
+    ASSERT_TRUE(reader->open(request).ok());
+
+    bool eof = false;
+    size_t total_rows = 0;
+    while (!eof) {
+        Block block = build_file_block(schema);
+        size_t rows = 0;
+        ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+        total_rows += rows;
+    }
+
+    EXPECT_EQ(total_rows, 0);
+    EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 6);
+    EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 6);
+    EXPECT_EQ(profile.get_counter("DictFilterCandidateColumns")->value(), 1);
+    EXPECT_EQ(profile.get_counter("DictFilterColumns")->value(), 1);
+    EXPECT_EQ(profile.get_counter("DictFilterUnsupportedColumns")->value(), 0);
+    EXPECT_EQ(profile.get_counter("DictFilterReadFailures")->value(), 0);
+    EXPECT_EQ(profile.get_counter("SelectedRows")->value(), 0);
+    // The first dictionary predicate column is read once to produce a compact 
row filter. The
+    // second predicate column is skipped after the selection becomes empty, 
which verifies the
+    // StarRocks-style round-by-round policy: only rows surviving previous 
predicates are read.
+    EXPECT_EQ(profile.get_counter("ReaderSelectRows")->value(), 6);
+    EXPECT_EQ(profile.get_counter("ReaderSkipRows")->value(), 6);
+}
+
+TEST_F(NewParquetReaderTest, 
DictionaryPredicateRunsResidualConjunctOnSurvivors) {
+    write_single_row_group_dictionary_filter_parquet_file(_file_path);
+
+    RuntimeProfile 
profile("new_parquet_reader_dictionary_prefilter_residual_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>();
+    request->predicate_columns = {field_projection(1)};
+    request->non_predicate_columns = {field_projection(0)};
+    request->conjuncts.push_back(
+            create_string_dictionary_and_residual_conjunct(1, {"az", "za"}, 
"za"));
+    use_schema_order_positions(request.get(), schema);
+    ASSERT_TRUE(reader->open(request).ok());
+
+    std::vector<int32_t> ids;
+    std::vector<std::string> values;
+    bool eof = false;
+    while (!eof) {
+        Block block = build_file_block(schema);
+        size_t rows = 0;
+        ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+        if (rows == 0) {
+            continue;
+        }
+        const auto& id_column = nullable_nested_column<ColumnInt32>(block, 0);
+        const auto& value_column = nullable_nested_column<ColumnString>(block, 
1);
+        for (size_t row = 0; row < rows; ++row) {
+            ids.push_back(id_column.get_element(row));
+            values.push_back(value_column.get_data_at(row).to_string());
+        }
+    }
+
+    EXPECT_EQ(ids, std::vector<int32_t>({5}));
+    EXPECT_EQ(values, std::vector<std::string>({"za"}));
+    EXPECT_EQ(profile.get_counter("RowsFilteredByDictFilter")->value(), 4);
+    EXPECT_EQ(profile.get_counter("RowsFilteredByConjunct")->value(), 1);

Review Comment:
   This expectation does not match the scanner counter path. The dictionary 
filter already removes 4 of the 6 rows, and `read_filter_columns()` adds those 
4 to `conjunct_filtered_rows`; then the residual `"za"` check removes one of 
the two survivors and adds one more. `read_current_row_group_batch()` publishes 
that accumulator as `RowsFilteredByConjunct`, so this test should see 5, not 1. 
Either update this expectation to match the current counter semantics, or stop 
adding dictionary-prefiltered rows to `conjunct_filtered_rows` and adjust the 
other dictionary profile assertions consistently.
   



##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -597,42 +780,500 @@ Status 
ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
     return Status::OK();
 }
 
+namespace {
+
+struct PredicateConjunctSchedule {
+    std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
+    VExprContextSPtrs remaining_conjuncts;
+};
+
+PredicateConjunctSchedule build_predicate_conjunct_schedule(
+        const format::FileScanRequest& request) {
+    std::unordered_set<size_t> predicate_block_positions;
+    predicate_block_positions.reserve(request.predicate_columns.size());
+    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());
+        predicate_block_positions.insert(position_it->second.value());
+    }
+
+    PredicateConjunctSchedule schedule;
+    for (const auto& conjunct : request.conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        DORIS_CHECK(conjunct->root() != nullptr);
+        if (!conjunct->root()->is_deterministic()) {

Review Comment:
   This guard still lets deterministic error-preserving predicates move onto 
the compacted survivor path. For example, `assert_true` is deterministic, but 
FE marks it as `NoneMovableFunction`, and the BE implementation throws when any 
evaluated row is false or null. Before this PR, every conjunct was executed 
with `rows=batch_rows`, so `a > 0 AND assert_true(b > 0, 'bad')` still checked 
`b` for rows rejected by `a > 0`. Now the later single-column conjunct can be 
scheduled here, `read_round_by_round()` reads/evaluates it only after 
`selected_rows` was shrunk by the earlier predicate, and those rejected rows no 
longer throw. Please keep error-preserving/non-movable functions on the old 
full-batch path, or add an execution-safety property that is stronger than 
determinism before scheduling a conjunct round by round.
   



-- 
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]

Reply via email to