Coverage Report

Created: 2026-07-20 21:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/parquet/parquet_scan.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//   http://www.apache.org/licenses/LICENSE-2.0
9
// Unless required by applicable law or agreed to in writing,
10
// software distributed under the License is distributed on an
11
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
12
// KIND, either express or implied.  See the License for the
13
// specific language governing permissions and limitations
14
// under the License.
15
16
#include "format_v2/parquet/parquet_scan.h"
17
18
#include <algorithm>
19
#include <iterator>
20
#include <limits>
21
#include <memory>
22
#include <optional>
23
#include <ranges>
24
#include <set>
25
#include <unordered_set>
26
#include <utility>
27
28
#include "common/exception.h"
29
#include "common/status.h"
30
#include "core/assert_cast.h"
31
#include "core/block/block.h"
32
#include "core/column/column_vector.h"
33
#include "exprs/vcompound_pred.h"
34
#include "exprs/vexpr_context.h"
35
#include "format_v2/parquet/parquet_column_schema.h"
36
#include "format_v2/parquet/parquet_file_context.h"
37
#include "format_v2/parquet/parquet_statistics.h"
38
#include "format_v2/parquet/reader/global_rowid_column_reader.h"
39
#include "format_v2/parquet/reader/native/column_chunk_reader.h"
40
#include "format_v2/parquet/reader/native_column_reader.h"
41
#include "format_v2/parquet/reader/row_position_column_reader.h"
42
#include "util/defer_op.h"
43
#include "util/time.h"
44
45
namespace doris::format::parquet {
46
47
namespace detail {
48
49
std::vector<size_t> order_adaptive_predicates(
50
        const std::vector<size_t>& positions,
51
90
        const std::unordered_map<size_t, AdaptivePredicateStats>& stats) {
52
90
    if (std::ranges::any_of(positions, [&](size_t position) {
53
81
            const auto it = stats.find(position);
54
81
            return it == stats.end() || it->second.samples == 0;
55
81
        })) {
56
46
        return positions;
57
46
    }
58
44
    auto ordered = positions;
59
44
    std::stable_sort(ordered.begin(), ordered.end(), [&](size_t left, size_t right) {
60
4
        const auto score = [&](size_t position) {
61
4
            const auto& sample = stats.at(position);
62
4
            return sample.cost_per_input_row_ns / std::max(1.0 - sample.survival_ratio, 0.01);
63
4
        };
64
2
        return score(left) < score(right);
65
2
    });
66
44
    return ordered;
67
90
}
68
69
std::vector<size_t> adaptive_prefetch_prefix(
70
        const std::vector<size_t>& ordered_positions,
71
        const std::unordered_map<size_t, AdaptivePredicateStats>& stats,
72
13
        double minimum_reach_probability) {
73
13
    if (std::ranges::any_of(ordered_positions, [&](size_t position) {
74
4
            const auto it = stats.find(position);
75
4
            return it == stats.end() || it->second.samples == 0;
76
4
        })) {
77
1
        return ordered_positions;
78
1
    }
79
12
    std::vector<size_t> result;
80
12
    double reach_probability = 1;
81
12
    for (const size_t position : ordered_positions) {
82
2
        if (!result.empty() && reach_probability < minimum_reach_probability) {
83
1
            break;
84
1
        }
85
1
        result.push_back(position);
86
1
        reach_probability *= stats.at(position).survival_ratio;
87
1
    }
88
12
    return result;
89
13
}
90
91
92
bool should_sample_adaptive_predicate(size_t samples, size_t batch_sequence) {
92
92
    constexpr size_t WARMUP_SAMPLES = 8;
93
92
    constexpr size_t STEADY_STATE_INTERVAL = 16;
94
92
    return samples < WARMUP_SAMPLES || batch_sequence % STEADY_STATE_INTERVAL == 0;
95
92
}
96
97
} // namespace detail
98
99
namespace {
100
101
detail::PredicateConjunctSchedule build_predicate_conjunct_schedule(
102
        const format::FileScanRequest& request);
103
104
12
bool is_dictionary_data_encoding(tparquet::Encoding::type encoding) {
105
12
    return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
106
12
           encoding == tparquet::Encoding::RLE_DICTIONARY;
107
12
}
108
109
0
bool is_level_encoding(tparquet::Encoding::type encoding) {
110
0
    return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED;
111
0
}
112
113
24
bool is_data_page_type(tparquet::PageType::type page_type) {
114
24
    return page_type == tparquet::PageType::DATA_PAGE ||
115
24
           page_type == tparquet::PageType::DATA_PAGE_V2;
116
24
}
117
118
12
bool is_fully_dictionary_encoded_chunk(const tparquet::ColumnMetaData& column_metadata) {
119
12
    if (!column_metadata.__isset.dictionary_page_offset ||
120
12
        column_metadata.dictionary_page_offset < 0) {
121
0
        return false;
122
0
    }
123
124
12
    const auto& encoding_stats = column_metadata.encoding_stats;
125
12
    if (!encoding_stats.empty()) {
126
12
        bool has_dictionary_data_page = false;
127
24
        for (const auto& encoding_stat : encoding_stats) {
128
24
            if (!is_data_page_type(encoding_stat.page_type) || encoding_stat.count <= 0) {
129
12
                continue;
130
12
            }
131
12
            if (!is_dictionary_data_encoding(encoding_stat.encoding)) {
132
0
                return false;
133
0
            }
134
12
            has_dictionary_data_page = true;
135
12
        }
136
12
        return has_dictionary_data_page;
137
12
    }
138
139
0
    bool has_dictionary_encoding = false;
140
0
    for (const auto encoding : column_metadata.encodings) {
141
0
        if (is_dictionary_data_encoding(encoding)) {
142
0
            has_dictionary_encoding = true;
143
0
            continue;
144
0
        }
145
0
        if (!is_level_encoding(encoding)) {
146
0
            return false;
147
0
        }
148
0
    }
149
0
    return has_dictionary_encoding;
150
0
}
151
152
bool supports_row_level_dictionary_filter(const ParquetColumnSchema& column_schema,
153
12
                                          const tparquet::ColumnMetaData& column_metadata) {
154
12
    if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE || column_schema.type == nullptr ||
155
12
        column_schema.max_repetition_level > 0) {
156
0
        return false;
157
0
    }
158
12
    if (!column_schema.type_descriptor.is_string_like ||
159
12
        column_metadata.type != tparquet::Type::BYTE_ARRAY) {
160
0
        return false;
161
0
    }
162
12
    if (remove_nullable(column_schema.type)->get_primitive_type() == TYPE_VARBINARY) {
163
        // A table STRING predicate can be rewritten through a raw VARBINARY file slot. Evaluating
164
        // it on dictionary Fields before the mapping expression is neither type-safe nor exact.
165
0
        return false;
166
0
    }
167
    // Row-level dictionary filtering consumes dictionary ids from DATA_PAGE payloads. It is exact
168
    // only when every data page is dictionary encoded. Mixed dictionary/plain chunks are left on
169
    // the normal decoded-value path, matching the safety rule used by StarRocks and Doris v1.
170
12
    return is_fully_dictionary_encoded_chunk(column_metadata);
171
12
}
172
173
void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
174
607
                                 std::unordered_set<int>* leaf_column_ids) {
175
607
    DORIS_CHECK(leaf_column_ids != nullptr);
176
607
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
177
561
        if (column_schema.leaf_column_id >= 0) {
178
561
            leaf_column_ids->insert(column_schema.leaf_column_id);
179
561
        }
180
561
        return;
181
561
    }
182
73
    for (const auto& child : column_schema.children) {
183
73
        DORIS_CHECK(child != nullptr);
184
73
        collect_all_leaf_column_ids(*child, leaf_column_ids);
185
73
    }
186
46
}
187
188
void collect_projected_leaf_column_ids(const ParquetColumnSchema& column_schema,
189
                                       const format::LocalColumnIndex& projection,
190
554
                                       std::unordered_set<int>* leaf_column_ids) {
191
554
    DORIS_CHECK(leaf_column_ids != nullptr);
192
554
    if (projection.project_all_children || projection.children.empty()) {
193
534
        collect_all_leaf_column_ids(column_schema, leaf_column_ids);
194
534
        return;
195
534
    }
196
22
    for (const auto& child_projection : projection.children) {
197
22
        const auto child_it =
198
38
                std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
199
38
                    return child_schema->local_id == child_projection.local_id();
200
38
                });
201
22
        DORIS_CHECK(child_it != column_schema.children.end());
202
22
        collect_projected_leaf_column_ids(**child_it, child_projection, leaf_column_ids);
203
22
    }
204
20
}
205
206
397
std::vector<format::LocalColumnIndex> request_scan_columns(const format::FileScanRequest& request) {
207
397
    std::vector<format::LocalColumnIndex> scan_columns;
208
397
    scan_columns.reserve(request.predicate_columns.size() + request.non_predicate_columns.size());
209
397
    scan_columns.insert(scan_columns.end(), request.predicate_columns.begin(),
210
397
                        request.predicate_columns.end());
211
412
    for (const auto& column : request.non_predicate_columns) {
212
412
        if (!request.is_count_star_placeholder(column.column_id())) {
213
404
            scan_columns.push_back(column);
214
404
        }
215
412
    }
216
397
    return scan_columns;
217
397
}
218
219
std::vector<format::LocalColumnIndex> physical_non_predicate_columns(
220
11
        const format::FileScanRequest& request) {
221
11
    std::vector<format::LocalColumnIndex> columns;
222
11
    columns.reserve(request.non_predicate_columns.size());
223
11
    for (const auto& column : request.non_predicate_columns) {
224
0
        if (!request.is_count_star_placeholder(column.column_id())) {
225
0
            columns.push_back(column);
226
0
        }
227
0
    }
228
11
    return columns;
229
11
}
230
231
void materialize_count_star_placeholders(const format::FileScanRequest& request, size_t rows,
232
174
                                         Block* file_block) {
233
174
    DORIS_CHECK(file_block != nullptr);
234
199
    for (const auto& column : request.non_predicate_columns) {
235
199
        if (!request.is_count_star_placeholder(column.column_id())) {
236
198
            continue;
237
198
        }
238
1
        const auto block_position = request.local_positions.at(column.column_id()).value();
239
1
        auto placeholder = file_block->get_by_position(block_position).column->assert_mutable();
240
1
        DCHECK(placeholder->empty());
241
1
        placeholder->insert_many_defaults(rows);
242
1
        file_block->replace_by_position(block_position, std::move(placeholder));
243
1
    }
244
174
}
245
246
} // namespace
247
248
namespace detail {
249
250
Status build_native_prefetch_ranges(
251
        const tparquet::FileMetaData& metadata,
252
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
253
        const std::vector<format::LocalColumnIndex>& scan_columns, int row_group_idx,
254
184
        size_t file_size, bool parquet_816_padding, std::vector<ParquetPageCacheRange>* ranges) {
255
184
    DORIS_CHECK(ranges != nullptr);
256
184
    ranges->clear();
257
184
    std::unordered_set<int> leaf_column_ids;
258
304
    for (const auto& projection : scan_columns) {
259
304
        const auto local_id = projection.local_id();
260
304
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
261
304
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
262
42
            continue;
263
42
        }
264
262
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size()) ||
265
262
            file_schema[local_id] == nullptr) {
266
0
            return Status::Corruption("Invalid Parquet projected column id {}", local_id);
267
0
        }
268
        // Prefetch and merge-reader ranges must be physical leaf chunks, not Doris logical slots.
269
        // Example: for a struct column s<a:int,b:string>, projecting only s.a should include only
270
        // the Parquet leaf chunk of a. Projecting the whole struct includes both a and b.
271
262
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &leaf_column_ids);
272
262
    }
273
274
184
    if (row_group_idx < 0 || row_group_idx >= static_cast<int>(metadata.row_groups.size())) {
275
0
        return Status::Corruption("Invalid Parquet row group index {}", row_group_idx);
276
0
    }
277
184
    const auto& row_group_metadata = metadata.row_groups[row_group_idx];
278
184
    std::vector<int> ordered_leaf_column_ids(leaf_column_ids.begin(), leaf_column_ids.end());
279
184
    std::ranges::sort(ordered_leaf_column_ids);
280
281
184
    ranges->reserve(ordered_leaf_column_ids.size());
282
276
    for (const auto leaf_column_id : ordered_leaf_column_ids) {
283
276
        if (leaf_column_id < 0 ||
284
276
            leaf_column_id >= static_cast<int>(row_group_metadata.columns.size())) {
285
0
            return Status::Corruption("Invalid Parquet leaf column id {}", leaf_column_id);
286
0
        }
287
276
        const auto& chunk = row_group_metadata.columns[leaf_column_id];
288
276
        if (!chunk.__isset.meta_data) {
289
0
            return Status::Corruption("Parquet leaf column {} has no chunk metadata",
290
0
                                      leaf_column_id);
291
0
        }
292
276
        native::ColumnChunkRange chunk_range;
293
276
        RETURN_IF_ERROR(native::compute_column_chunk_range(chunk.meta_data, file_size,
294
276
                                                           parquet_816_padding, &chunk_range));
295
275
        if (chunk_range.length > 0) {
296
275
            if (chunk_range.offset > static_cast<size_t>(std::numeric_limits<int64_t>::max()) ||
297
275
                chunk_range.length > static_cast<size_t>(std::numeric_limits<int64_t>::max())) {
298
0
                return Status::Corruption("Parquet column chunk range exceeds int64 coordinates");
299
0
            }
300
            // Prefetch must use the same checked chunk extent as the decoder, including the
301
            // PARQUET-816 compatibility padding, so warm-up cannot target different bytes.
302
275
            ranges->push_back(
303
275
                    ParquetPageCacheRange {.offset = static_cast<int64_t>(chunk_range.offset),
304
275
                                           .size = static_cast<int64_t>(chunk_range.length)});
305
275
        }
306
275
    }
307
183
    return Status::OK();
308
184
}
309
310
} // namespace detail
311
312
namespace detail {
313
314
Status select_native_row_groups_by_scan_range(const tparquet::FileMetaData& metadata,
315
                                              const ParquetScanRange& scan_range,
316
                                              std::vector<int64_t>* row_group_first_rows,
317
192
                                              std::vector<int>* selected_row_groups) {
318
192
    DORIS_CHECK(row_group_first_rows != nullptr && selected_row_groups != nullptr);
319
192
    if (scan_range.start_offset < 0 || scan_range.size < -1 ||
320
192
        (scan_range.size >= 0 &&
321
192
         scan_range.start_offset > std::numeric_limits<int64_t>::max() - scan_range.size)) {
322
0
        return Status::Corruption("Invalid Parquet scan range [{}, {})", scan_range.start_offset,
323
0
                                  scan_range.size);
324
0
    }
325
192
    const uint64_t range_start = static_cast<uint64_t>(scan_range.start_offset);
326
192
    const uint64_t range_end = scan_range.size < 0
327
192
                                       ? std::numeric_limits<uint64_t>::max()
328
192
                                       : range_start + static_cast<uint64_t>(scan_range.size);
329
192
    const size_t file_size = scan_range.file_size < 0 ? std::numeric_limits<size_t>::max()
330
192
                                                      : static_cast<size_t>(scan_range.file_size);
331
192
    const bool full_file_range =
332
192
            scan_range.size < 0 || (range_start == 0 && scan_range.file_size >= 0 &&
333
9
                                    range_end >= static_cast<uint64_t>(scan_range.file_size));
334
192
    const auto compat = native::parquet_reader_compat(
335
192
            metadata.__isset.created_by ? metadata.created_by : std::string {});
336
192
    row_group_first_rows->assign(metadata.row_groups.size(), 0);
337
192
    selected_row_groups->clear();
338
192
    selected_row_groups->reserve(metadata.row_groups.size());
339
192
    int64_t next_first_row = 0;
340
468
    for (size_t row_group_idx = 0; row_group_idx < metadata.row_groups.size(); ++row_group_idx) {
341
276
        (*row_group_first_rows)[row_group_idx] = next_first_row;
342
276
        const auto& row_group = metadata.row_groups[row_group_idx];
343
276
        if (row_group.num_rows < 0) {
344
0
            return Status::Corruption("Invalid negative row count in parquet row group {}",
345
0
                                      row_group_idx);
346
0
        }
347
276
        if (row_group.num_rows > std::numeric_limits<int64_t>::max() - next_first_row) {
348
0
            return Status::Corruption("Parquet row counts overflow at row group {}", row_group_idx);
349
0
        }
350
276
        next_first_row += row_group.num_rows;
351
276
        bool selected = full_file_range;
352
276
        if (!full_file_range) {
353
23
            if (row_group.columns.empty()) {
354
0
                return Status::Corruption("Parquet row group {} has no column chunks",
355
0
                                          row_group_idx);
356
0
            }
357
23
            size_t group_start = std::numeric_limits<size_t>::max();
358
23
            size_t group_end = 0;
359
76
            for (size_t column_idx = 0; column_idx < row_group.columns.size(); ++column_idx) {
360
53
                const auto& chunk = row_group.columns[column_idx];
361
53
                if (!chunk.__isset.meta_data) {
362
0
                    return Status::Corruption("Parquet row group {} column {} has no metadata",
363
0
                                              row_group_idx, column_idx);
364
0
                }
365
53
                native::ColumnChunkRange chunk_range;
366
53
                RETURN_IF_ERROR(native::compute_column_chunk_range(
367
53
                        chunk.meta_data, file_size, compat.parquet_816_padding, &chunk_range));
368
53
                group_start = std::min(group_start, chunk_range.offset);
369
53
                group_end = std::max(group_end, chunk_range.offset + chunk_range.length);
370
53
            }
371
            // Checked chunk ranges make end >= start; this midpoint form cannot overflow even
372
            // when footer offsets are close to the host coordinate limit.
373
23
            const uint64_t group_mid =
374
23
                    static_cast<uint64_t>(group_start) + (group_end - group_start) / 2;
375
23
            selected = group_mid >= range_start && group_mid < range_end;
376
23
        }
377
276
        if (selected) {
378
261
            selected_row_groups->push_back(cast_set<int>(row_group_idx));
379
261
        }
380
276
    }
381
192
    return Status::OK();
382
192
}
383
384
} // namespace detail
385
386
namespace {
387
388
std::vector<RowRange> intersect_row_ranges(const std::vector<RowRange>& left,
389
216
                                           const std::vector<RowRange>& right) {
390
216
    std::vector<RowRange> result;
391
216
    size_t left_idx = 0;
392
216
    size_t right_idx = 0;
393
432
    while (left_idx < left.size() && right_idx < right.size()) {
394
216
        const int64_t left_end = left[left_idx].start + left[left_idx].length;
395
216
        const int64_t right_end = right[right_idx].start + right[right_idx].length;
396
216
        const int64_t start = std::max(left[left_idx].start, right[right_idx].start);
397
216
        const int64_t end = std::min(left_end, right_end);
398
216
        if (start < end) {
399
216
            result.push_back({.start = start, .length = end - start});
400
216
        }
401
216
        if (left_end < right_end) {
402
0
            ++left_idx;
403
216
        } else {
404
216
            ++right_idx;
405
216
        }
406
216
    }
407
216
    return result;
408
216
}
409
410
Status finalize_native_row_group_read_plan(
411
        const NativeParquetMetadata& metadata,
412
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
413
        const format::FileScanRequest& request, bool enable_bloom_filter,
414
        RowGroupReadPlan* row_group_plan, ParquetPruningStats* pruning_stats,
415
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
416
        ParquetFileContext* file_context, const ParquetColumnReaderProfile& column_reader_profile,
417
248
        bool* selected) {
418
248
    DORIS_CHECK(row_group_plan != nullptr && pruning_stats != nullptr && file_context != nullptr &&
419
248
                selected != nullptr);
420
248
    *selected = true;
421
248
    if (!row_group_plan->expensive_pruning_pending) {
422
10
        return Status::OK();
423
10
    }
424
238
    row_group_plan->expensive_pruning_pending = false;
425
238
    const auto& thrift = metadata.to_thrift();
426
238
    const std::vector<int> candidate {row_group_plan->row_group_id};
427
238
    std::vector<int> metadata_selected;
428
238
    RETURN_IF_ERROR(select_row_groups_by_metadata(
429
238
            thrift, file_schema, request, &candidate, &metadata_selected, enable_bloom_filter,
430
238
            pruning_stats, timezone, runtime_state, file_context, column_reader_profile,
431
238
            ParquetMetadataProbeMode::EXPENSIVE_ONLY));
432
238
    if (metadata_selected.empty()) {
433
22
        *selected = false;
434
22
        return Status::OK();
435
22
    }
436
437
216
    std::unordered_set<int> requested_leaf_ids;
438
311
    for (const auto& projection : request_scan_columns(request)) {
439
311
        const auto local_id = projection.local_id();
440
311
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size())) {
441
41
            continue;
442
41
        }
443
270
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &requested_leaf_ids);
444
270
    }
445
216
    std::unordered_map<int, NativeParquetPageIndex> page_indexes;
446
216
    if (can_use_parquet_page_index(request, runtime_state)) {
447
44
        RETURN_IF_ERROR(file_context->load_native_page_indexes(
448
44
                row_group_plan->row_group_id, requested_leaf_ids, &page_indexes,
449
44
                &pruning_stats->read_page_index_time, &pruning_stats->parse_page_index_time));
450
44
    }
451
216
    std::vector<RowRange> page_selected_ranges;
452
216
    std::map<int, ParquetPageSkipPlan> page_skip_plans;
453
216
    RETURN_IF_ERROR(select_row_group_ranges_by_native_page_index(
454
216
            thrift, page_indexes, file_schema, request, row_group_plan->row_group_rows,
455
216
            &page_selected_ranges, &page_skip_plans, pruning_stats, timezone, runtime_state));
456
216
    row_group_plan->selected_ranges =
457
216
            intersect_row_ranges(row_group_plan->selected_ranges, page_selected_ranges);
458
216
    row_group_plan->page_skip_plans = std::move(page_skip_plans);
459
216
    for (auto& [leaf_column_id, indexes] : page_indexes) {
460
4
        row_group_plan->offset_indexes.emplace(leaf_column_id, std::move(indexes.offset_index));
461
4
    }
462
216
    if (row_group_plan->selected_ranges.empty()) {
463
0
        *selected = false;
464
0
        return Status::OK();
465
0
    }
466
216
    pruning_stats->selected_row_ranges += row_group_plan->selected_ranges.size();
467
216
    return Status::OK();
468
216
}
469
470
Status build_native_row_group_read_plans(
471
        const NativeParquetMetadata& metadata,
472
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
473
        const format::FileScanRequest& request, const std::vector<int>& selected_row_groups,
474
        const std::vector<int64_t>& row_group_first_rows, RowGroupScanPlan* plan,
475
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
476
190
        ParquetFileContext* file_context) {
477
190
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
478
190
    const auto& thrift = metadata.to_thrift();
479
190
    plan->row_groups.reserve(selected_row_groups.size());
480
242
    for (const int row_group_idx : selected_row_groups) {
481
242
        const auto& row_group = thrift.row_groups[row_group_idx];
482
242
        if (row_group.num_rows == 0) {
483
0
            continue;
484
0
        }
485
242
        RowGroupReadPlan row_group_plan;
486
242
        row_group_plan.row_group_id = row_group_idx;
487
242
        row_group_plan.first_file_row = row_group_first_rows[row_group_idx];
488
242
        row_group_plan.row_group_rows = row_group.num_rows;
489
242
        row_group_plan.selected_ranges = {{.start = 0, .length = row_group.num_rows}};
490
242
        row_group_plan.expensive_pruning_pending = true;
491
242
        plan->row_groups.push_back(std::move(row_group_plan));
492
242
    }
493
190
    return Status::OK();
494
190
}
495
496
} // namespace
497
498
Status plan_parquet_row_groups(const NativeParquetMetadata& metadata,
499
                               const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
500
                               const format::FileScanRequest& request,
501
                               const ParquetScanRange& scan_range, bool enable_bloom_filter,
502
                               RowGroupScanPlan* plan, const cctz::time_zone* timezone,
503
                               const RuntimeState* runtime_state, ParquetFileContext* file_context,
504
190
                               const ParquetColumnReaderProfile& column_reader_profile) {
505
190
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
506
190
    plan->row_groups.clear();
507
190
    plan->pruning_stats = {};
508
190
    plan->enable_bloom_filter = enable_bloom_filter;
509
190
    std::vector<int64_t> row_group_first_rows;
510
190
    std::vector<int> scan_range_selected;
511
190
    RETURN_IF_ERROR(detail::select_native_row_groups_by_scan_range(
512
190
            metadata.to_thrift(), scan_range, &row_group_first_rows, &scan_range_selected));
513
190
    std::vector<int> metadata_selected;
514
190
    RETURN_IF_ERROR(select_row_groups_by_metadata(
515
190
            metadata.to_thrift(), file_schema, request, &scan_range_selected, &metadata_selected,
516
190
            enable_bloom_filter, &plan->pruning_stats, timezone, runtime_state, file_context,
517
190
            column_reader_profile, ParquetMetadataProbeMode::FOOTER_ONLY));
518
190
    RETURN_IF_ERROR(build_native_row_group_read_plans(metadata, file_schema, request,
519
190
                                                      metadata_selected, row_group_first_rows, plan,
520
190
                                                      timezone, runtime_state, file_context));
521
190
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
522
190
    return Status::OK();
523
190
}
524
525
Status finalize_parquet_row_group_plans(
526
        const NativeParquetMetadata& metadata,
527
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
528
        const format::FileScanRequest& request, bool enable_bloom_filter, RowGroupScanPlan* plan,
529
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
530
        ParquetFileContext* file_context, const ParquetColumnReaderProfile& column_reader_profile,
531
28
        const ParquetProfile* parquet_profile) {
532
28
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
533
28
    std::vector<RowGroupReadPlan> selected_plans;
534
28
    selected_plans.reserve(plan->row_groups.size());
535
45
    for (auto& row_group_plan : plan->row_groups) {
536
45
        ParquetPruningStats deferred_stats;
537
45
        bool selected = false;
538
45
        RETURN_IF_ERROR(finalize_native_row_group_read_plan(
539
45
                metadata, file_schema, request, enable_bloom_filter, &row_group_plan,
540
45
                &deferred_stats, timezone, runtime_state, file_context, column_reader_profile,
541
45
                &selected));
542
45
        if (parquet_profile != nullptr) {
543
5
            parquet_profile->update_deferred_pruning_stats(deferred_stats, selected);
544
5
        }
545
45
        if (selected) {
546
45
            selected_plans.push_back(std::move(row_group_plan));
547
45
        }
548
45
    }
549
28
    plan->row_groups = std::move(selected_plans);
550
28
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
551
28
    return Status::OK();
552
28
}
553
554
namespace {
555
556
using DictionaryResidualConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
557
using DictionaryResidualConjuncts = std::vector<DictionaryResidualConjunct>;
558
559
211
void update_counter_if_not_null(RuntimeProfile::Counter* counter, int64_t value) {
560
211
    if (counter != nullptr) {
561
136
        COUNTER_UPDATE(counter, value);
562
136
    }
563
211
}
564
565
uint16_t apply_filter_to_selection(const IColumn::Filter& filter, SelectionVector* selection,
566
35
                                   uint16_t selected_rows) {
567
35
    uint16_t new_selected_rows = 0;
568
161
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
569
126
        const auto row_idx = selection->get_index(selection_idx);
570
126
        if (filter[row_idx] != 0) {
571
83
            selection->set_index(new_selected_rows++, static_cast<SelectionVector::Index>(row_idx));
572
83
        }
573
126
    }
574
35
    return new_selected_rows;
575
35
}
576
577
Status execute_compact_filter_conjuncts(const VExprContextSPtrs& conjuncts, size_t rows,
578
                                        Block* file_block, IColumn::Filter* compact_filter,
579
66
                                        bool* can_filter_all) {
580
66
    DORIS_CHECK(compact_filter != nullptr);
581
66
    DORIS_CHECK(can_filter_all != nullptr);
582
66
    compact_filter->resize_fill(rows, 1);
583
66
    *can_filter_all = false;
584
66
    for (const auto& conjunct : conjuncts) {
585
66
        DORIS_CHECK(conjunct != nullptr);
586
66
        IColumn::Filter filter(rows, 1);
587
66
        bool conjunct_can_filter_all = false;
588
66
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(), rows, false,
589
66
                                                 &conjunct_can_filter_all));
590
66
        if (conjunct_can_filter_all) {
591
28
            std::ranges::fill(*compact_filter, 0);
592
28
            *can_filter_all = true;
593
28
            break;
594
28
        }
595
9.67k
        for (size_t row = 0; row < rows; ++row) {
596
9.64k
            (*compact_filter)[row] &= filter[row];
597
9.64k
        }
598
38
    }
599
66
    return Status::OK();
600
66
}
601
602
Status execute_compact_dictionary_residual_conjuncts(const DictionaryResidualConjuncts& conjuncts,
603
                                                     size_t rows, Block* file_block,
604
                                                     IColumn::Filter* compact_filter,
605
3
                                                     bool* can_filter_all) {
606
3
    DORIS_CHECK(compact_filter != nullptr);
607
3
    DORIS_CHECK(can_filter_all != nullptr);
608
3
    compact_filter->resize_fill(rows, 1);
609
3
    *can_filter_all = false;
610
3
    for (const auto& [owner_context, residual_expr] : conjuncts) {
611
3
        DORIS_CHECK(owner_context != nullptr);
612
3
        DORIS_CHECK(residual_expr != nullptr);
613
3
        IColumn::Filter filter(rows, 1);
614
3
        bool conjunct_can_filter_all = false;
615
3
        RETURN_IF_ERROR(residual_expr->execute_filter(owner_context.get(), file_block,
616
3
                                                      filter.data(), rows, false,
617
3
                                                      &conjunct_can_filter_all));
618
3
        if (conjunct_can_filter_all) {
619
1
            std::ranges::fill(*compact_filter, 0);
620
1
            *can_filter_all = true;
621
1
            break;
622
1
        }
623
6
        for (size_t row = 0; row < rows; ++row) {
624
4
            (*compact_filter)[row] &= filter[row];
625
4
        }
626
2
    }
627
3
    return Status::OK();
628
3
}
629
630
Status execute_compact_delete_conjuncts(const VExprContextSPtrs& delete_conjuncts, size_t rows,
631
                                        Block* file_block, IColumn::Filter* compact_filter,
632
1
                                        bool* can_filter_all) {
633
1
    DORIS_CHECK(compact_filter != nullptr);
634
1
    DORIS_CHECK(can_filter_all != nullptr);
635
1
    compact_filter->resize_fill(rows, 1);
636
1
    *can_filter_all = false;
637
1
    for (const auto& delete_conjunct : delete_conjuncts) {
638
1
        DORIS_CHECK(delete_conjunct != nullptr);
639
1
        int result_column_id = -1;
640
1
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
641
1
                                                         &result_column_id));
642
1
        DORIS_CHECK(result_column_id >= 0 &&
643
1
                    result_column_id < static_cast<int>(file_block->columns()));
644
1
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
645
1
                                            *file_block->get_by_position(result_column_id).column)
646
1
                                            .get_data();
647
1
        DORIS_CHECK(delete_filter.size() == rows);
648
1
        bool has_kept_row = false;
649
4
        for (size_t row = 0; row < rows; ++row) {
650
3
            (*compact_filter)[row] &= !delete_filter[row];
651
3
            has_kept_row |= (*compact_filter)[row] != 0;
652
3
        }
653
1
        file_block->erase(result_column_id);
654
1
        if (!has_kept_row) {
655
0
            *can_filter_all = true;
656
0
            break;
657
0
        }
658
1
    }
659
1
    return Status::OK();
660
1
}
661
662
Status execute_filter_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
663
                                Block* file_block, SelectionVector* selection,
664
39
                                uint16_t* selected_rows) {
665
39
    for (const auto& conjunct : request.conjuncts) {
666
7
        if (*selected_rows == 0) {
667
0
            break;
668
0
        }
669
7
        DORIS_CHECK(conjunct != nullptr);
670
7
        IColumn::Filter filter(static_cast<size_t>(batch_rows), 1);
671
7
        bool can_filter_all = false;
672
7
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(),
673
7
                                                 static_cast<size_t>(batch_rows), false,
674
7
                                                 &can_filter_all));
675
7
        *selected_rows =
676
7
                can_filter_all ? 0 : apply_filter_to_selection(filter, selection, *selected_rows);
677
7
    }
678
39
    return Status::OK();
679
39
}
680
681
Status execute_delete_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
682
                                Block* file_block, SelectionVector* selection,
683
39
                                uint16_t* selected_rows) {
684
39
    for (const auto& delete_conjunct : request.delete_conjuncts) {
685
34
        if (*selected_rows == 0) {
686
0
            break;
687
0
        }
688
34
        DORIS_CHECK(delete_conjunct != nullptr);
689
34
        int result_column_id = -1;
690
34
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
691
34
                                                         &result_column_id));
692
34
        DORIS_CHECK(result_column_id >= 0 &&
693
34
                    result_column_id < static_cast<int>(file_block->columns()));
694
34
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
695
34
                                            *file_block->get_by_position(result_column_id).column)
696
34
                                            .get_data();
697
34
        DORIS_CHECK(delete_filter.size() == static_cast<size_t>(batch_rows));
698
34
        IColumn::Filter keep_filter(static_cast<size_t>(batch_rows), 1);
699
34
        bool has_kept_row = false;
700
148
        for (size_t row = 0; row < static_cast<size_t>(batch_rows); ++row) {
701
114
            keep_filter[row] = !delete_filter[row];
702
114
            has_kept_row |= keep_filter[row] != 0;
703
114
        }
704
34
        file_block->erase(result_column_id);
705
34
        *selected_rows =
706
34
                !has_kept_row ? 0
707
34
                              : apply_filter_to_selection(keep_filter, selection, *selected_rows);
708
34
    }
709
39
    return Status::OK();
710
39
}
711
712
} // namespace
713
714
uint16_t apply_compact_filter_to_selection(const IColumn::Filter& filter,
715
30
                                           SelectionVector* selection, uint16_t selected_rows) {
716
30
    DORIS_CHECK(selection != nullptr);
717
30
    DORIS_CHECK(filter.size() == selected_rows);
718
30
    uint16_t new_selected_rows = 0;
719
7.40k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
720
7.37k
        if (filter[selection_idx] != 0) {
721
2.11k
            selection->set_index(new_selected_rows++, static_cast<SelectionVector::Index>(
722
2.11k
                                                              selection->get_index(selection_idx)));
723
2.11k
        }
724
7.37k
    }
725
30
    return new_selected_rows;
726
30
}
727
728
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t selected_rows,
729
38
                                    int64_t batch_rows) {
730
38
    IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
731
112
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
732
74
        filter[selection.get_index(selection_idx)] = 1;
733
74
    }
734
38
    return filter;
735
38
}
736
737
Status execute_batch_filters(const format::FileScanRequest& request, int64_t batch_rows,
738
                             Block* file_block, SelectionVector* selection, uint16_t* selected_rows,
739
121
                             int64_t* conjunct_filtered_rows) {
740
121
    if (request.conjuncts.empty() && request.delete_conjuncts.empty()) {
741
82
        return Status::OK();
742
82
    }
743
39
    const auto selected_rows_before_conjunct = *selected_rows;
744
39
    RETURN_IF_ERROR(
745
39
            execute_filter_conjuncts(request, batch_rows, file_block, selection, selected_rows));
746
39
    if (conjunct_filtered_rows != nullptr) {
747
39
        *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before_conjunct) -
748
39
                                   static_cast<int64_t>(*selected_rows);
749
39
    }
750
39
    if (*selected_rows == 0) {
751
0
        return Status::OK();
752
0
    }
753
39
    return execute_delete_conjuncts(request, batch_rows, file_block, selection, selected_rows);
754
39
}
755
756
namespace {
757
4
int64_t count_range_rows(const std::vector<RowRange>& ranges) {
758
4
    int64_t rows = 0;
759
4
    for (const auto& range : ranges) {
760
4
        rows += range.length;
761
4
    }
762
4
    return rows;
763
4
}
764
765
void append_intersection(const RowRange& left, const RowRange& right,
766
2
                         std::vector<RowRange>* result) {
767
2
    const int64_t start = std::max(left.start, right.start);
768
2
    const int64_t end = std::min(left.start + left.length, right.start + right.length);
769
2
    if (start < end) {
770
2
        result->push_back(RowRange {.start = start, .length = end - start});
771
2
    }
772
2
}
773
774
std::vector<RowRange> filter_ranges_by_condition_cache(const std::vector<RowRange>& ranges,
775
                                                       const std::vector<bool>& cache,
776
                                                       int64_t row_group_first_row,
777
2
                                                       int64_t base_granule) {
778
2
    std::vector<RowRange> result;
779
2
    if (cache.empty()) {
780
0
        return ranges;
781
0
    }
782
783
    // Cache coordinates are file-global granules; RowRange coordinates are row-group-relative.
784
    // Walk every selected range in order and split it by granule. Granules covered by the bitmap
785
    // are kept only when the bit is true. Granules outside the bitmap are kept conservatively, so
786
    // an undersized or old-format cache entry cannot skip valid rows.
787
2
    for (const auto& range : ranges) {
788
2
        const int64_t global_start = row_group_first_row + range.start;
789
2
        const int64_t global_end = global_start + range.length;
790
2
        for (int64_t granule = global_start / ConditionCacheContext::GRANULE_SIZE;
791
5
             granule <= (global_end - 1) / ConditionCacheContext::GRANULE_SIZE; ++granule) {
792
3
            const int64_t cache_idx = granule - base_granule;
793
3
            const bool keep = cache_idx < 0 || static_cast<size_t>(cache_idx) >= cache.size() ||
794
3
                              cache[static_cast<size_t>(cache_idx)];
795
3
            if (!keep) {
796
1
                continue;
797
1
            }
798
2
            const int64_t granule_start = granule * ConditionCacheContext::GRANULE_SIZE;
799
2
            const int64_t granule_end = granule_start + ConditionCacheContext::GRANULE_SIZE;
800
2
            const RowRange file_granule_range {.start = granule_start - row_group_first_row,
801
2
                                               .length = granule_end - granule_start};
802
2
            append_intersection(range, file_granule_range, &result);
803
2
        }
804
2
    }
805
2
    return result;
806
2
}
807
808
} // namespace
809
810
191
void ParquetScanScheduler::set_plan(RowGroupScanPlan plan) {
811
191
    _enable_bloom_filter = plan.enable_bloom_filter;
812
191
    _row_group_plans = std::move(plan.row_groups);
813
191
    _condition_cache_filtered_rows = 0;
814
191
    _predicate_filtered_rows = 0;
815
191
    reset();
816
191
}
817
818
3
void ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) {
819
3
    _condition_cache_ctx = std::move(ctx);
820
3
    if (!_condition_cache_ctx || !_condition_cache_ctx->filter_result || _row_group_plans.empty()) {
821
0
        return;
822
0
    }
823
824
3
    if (!_condition_cache_ctx->is_hit) {
825
1
        _condition_cache_ctx->base_granule =
826
1
                _row_group_plans.front().first_file_row / ConditionCacheContext::GRANULE_SIZE;
827
1
        const auto& last_plan = _row_group_plans.back();
828
1
        const int64_t end_granule = (last_plan.first_file_row + last_plan.row_group_rows +
829
1
                                     ConditionCacheContext::GRANULE_SIZE - 1) /
830
1
                                    ConditionCacheContext::GRANULE_SIZE;
831
1
        DORIS_CHECK(end_granule > _condition_cache_ctx->base_granule);
832
1
        _condition_cache_ctx->num_granules =
833
1
                std::min(_condition_cache_ctx->filter_result->size(),
834
1
                         static_cast<size_t>(end_granule - _condition_cache_ctx->base_granule));
835
1
        return;
836
1
    }
837
838
2
    std::vector<RowGroupReadPlan> filtered_plans;
839
2
    filtered_plans.reserve(_row_group_plans.size());
840
2
    for (auto& plan : _row_group_plans) {
841
2
        const int64_t old_rows = count_range_rows(plan.selected_ranges);
842
2
        plan.selected_ranges = filter_ranges_by_condition_cache(
843
2
                plan.selected_ranges, *_condition_cache_ctx->filter_result, plan.first_file_row,
844
2
                _condition_cache_ctx->base_granule);
845
2
        const int64_t new_rows = count_range_rows(plan.selected_ranges);
846
2
        _condition_cache_filtered_rows += old_rows - new_rows;
847
2
        if (!plan.selected_ranges.empty()) {
848
2
            filtered_plans.push_back(std::move(plan));
849
2
        }
850
2
    }
851
2
    _row_group_plans = std::move(filtered_plans);
852
2
    reset();
853
2
}
854
855
193
void ParquetScanScheduler::reset() {
856
193
    _next_row_group_plan_idx = 0;
857
193
    _raw_rows_read = 0;
858
193
    _predicate_schedule_request = nullptr;
859
193
    _predicate_schedule = {};
860
193
    _predicate_positions_scratch.clear();
861
193
    _predicate_indices_by_position_scratch.clear();
862
193
    _ordered_predicate_positions_scratch.clear();
863
193
    _predicate_batch_sequence = 0;
864
193
    reset_current_row_group();
865
193
}
866
867
420
void ParquetScanScheduler::reset_current_row_group() {
868
    // RuntimeProfile updates are amortized on the batch path, but a row-group transition destroys
869
    // the reader tree. Force the final delta out before clearing it so short row groups and early
870
    // EOF paths cannot lose their last decode/IO timings.
871
420
    flush_current_reader_profiles();
872
420
    _batches_since_profile_flush = 0;
873
420
    _has_current_row_group = false;
874
420
    _current_predicate_columns.clear();
875
420
    _current_non_predicate_columns.clear();
876
420
    _current_dictionary_filters.clear();
877
420
    _current_dictionary_residual_conjuncts.clear();
878
420
    _current_row_group_rows = 0;
879
420
    _current_row_group_id = -1;
880
420
    _current_row_group_rows_read = 0;
881
420
    _current_row_group_first_row = 0;
882
420
    _current_selected_ranges.clear();
883
420
    _current_offset_indexes.clear();
884
420
    _current_range_idx = 0;
885
420
    _current_range_rows_read = 0;
886
    // Readers are row-group scoped. If every remaining row was filtered, no future output can
887
    // observe the non-predicate readers' position, so dropping them together with their pending lag
888
    // avoids a useless end-of-row-group SkipRecords call. Example: predicate readers advance from 0
889
    // to 10,000 while lazy readers stay at 0; clearing both readers here is sufficient because the
890
    // next row group constructs a new set starting at its own row 0.
891
420
    _pending_non_predicate_skip_rows = 0;
892
420
    _current_predicate_prefetched = false;
893
420
    _current_non_predicate_prefetched = false;
894
420
    _current_merge_range_active = false;
895
420
}
896
897
603
void ParquetScanScheduler::flush_current_reader_profiles() {
898
603
    for (const auto& reader : _current_predicate_columns | std::views::values) {
899
189
        reader->flush_profile();
900
189
    }
901
603
    for (const auto& reader : _current_non_predicate_columns | std::views::values) {
902
376
        reader->flush_profile();
903
376
    }
904
603
}
905
906
210
bool ParquetScanScheduler::finish_current_reader_batch_profiles() {
907
210
    bool crossed_page = false;
908
    // A scheduler batch is counted once even when several projected leaves cross page boundaries.
909
210
    for (const auto& reader : _current_predicate_columns | std::views::values) {
910
128
        crossed_page |= reader->crossed_page_since_last_batch();
911
128
    }
912
251
    for (const auto& reader : _current_non_predicate_columns | std::views::values) {
913
251
        crossed_page |= reader->crossed_page_since_last_batch();
914
251
    }
915
210
    return crossed_page;
916
210
}
917
918
const detail::PredicateConjunctSchedule& ParquetScanScheduler::predicate_conjunct_schedule(
919
257
        const format::FileScanRequest& request) {
920
257
    if (_predicate_schedule_request == &request) {
921
105
        return _predicate_schedule;
922
105
    }
923
924
    // FileScanRequest is frozen by ParquetReader::open(). Its address therefore identifies both
925
    // the conjunct set and local-position mapping for the scheduler lifetime.
926
152
    _predicate_schedule = build_predicate_conjunct_schedule(request);
927
152
    _predicate_schedule_request = &request;
928
152
    _predicate_positions_scratch.clear();
929
152
    _predicate_indices_by_position_scratch.clear();
930
152
    _predicate_positions_scratch.reserve(request.predicate_columns.size());
931
152
    _predicate_indices_by_position_scratch.reserve(request.predicate_columns.size());
932
247
    for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
933
95
        const auto position_it =
934
95
                request.local_positions.find(request.predicate_columns[idx].column_id());
935
95
        DORIS_CHECK(position_it != request.local_positions.end());
936
95
        const size_t position = position_it->second.value();
937
95
        _predicate_positions_scratch.push_back(position);
938
95
        _predicate_indices_by_position_scratch.emplace(position, idx);
939
95
    }
940
152
    return _predicate_schedule;
941
257
}
942
943
std::vector<format::LocalColumnIndex> ParquetScanScheduler::adaptive_predicate_prefetch_columns(
944
12
        const format::FileScanRequest& request) const {
945
12
    std::vector<size_t> positions;
946
12
    std::unordered_map<size_t, const format::LocalColumnIndex*> columns_by_position;
947
12
    positions.reserve(request.predicate_columns.size());
948
12
    columns_by_position.reserve(request.predicate_columns.size());
949
12
    for (const auto& column : request.predicate_columns) {
950
1
        const auto position_it = request.local_positions.find(column.column_id());
951
1
        DORIS_CHECK(position_it != request.local_positions.end());
952
1
        const size_t position = position_it->second.value();
953
1
        positions.push_back(position);
954
1
        columns_by_position.emplace(position, &column);
955
1
    }
956
12
    auto ordered = detail::order_adaptive_predicates(positions, _predicate_runtime_stats);
957
12
    ordered = detail::adaptive_prefetch_prefix(ordered, _predicate_runtime_stats, 0.25);
958
12
    std::vector<format::LocalColumnIndex> result;
959
12
    result.reserve(ordered.size());
960
12
    for (const size_t position : ordered) {
961
1
        result.push_back(*columns_by_position.at(position));
962
1
    }
963
12
    return result;
964
12
}
965
966
Status ParquetScanScheduler::open_next_row_group(
967
        ParquetFileContext& file_context,
968
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
969
280
        const format::FileScanRequest& request, bool* has_row_group) {
970
280
    *has_row_group = false;
971
280
    RowGroupReadPlan* selected_plan = nullptr;
972
302
    while (_next_row_group_plan_idx < _row_group_plans.size()) {
973
203
        RowGroupReadPlan& candidate_plan = _row_group_plans[_next_row_group_plan_idx++];
974
        // Probe only the row group about to execute. This keeps LIMIT/cancellation latency
975
        // independent of the number of later remote row groups while preserving eager footer
976
        // statistics pruning during open.
977
203
        file_context.reset_random_access_ranges();
978
203
        _current_merge_range_active = false;
979
203
        ParquetPruningStats deferred_stats;
980
203
        bool selected = false;
981
203
        RETURN_IF_ERROR(finalize_native_row_group_read_plan(
982
203
                *file_context.native_metadata, file_schema, request, _enable_bloom_filter,
983
203
                &candidate_plan, &deferred_stats, _timezone, _runtime_state, &file_context,
984
203
                _scan_profile.column_reader_profile, &selected));
985
203
        if (_parquet_profile != nullptr) {
986
108
            _parquet_profile->update_deferred_pruning_stats(deferred_stats, selected);
987
108
        }
988
203
        if (!selected) {
989
22
            continue;
990
22
        }
991
181
        selected_plan = &candidate_plan;
992
181
        break;
993
203
    }
994
280
    if (selected_plan == nullptr) {
995
        // The last row group's native readers have already been released by
996
        // reset_current_row_group(). Flush the shared merge reader now so its counters are visible
997
        // when EOF is returned and its bounded scratch does not survive until file close.
998
99
        file_context.reset_random_access_ranges();
999
99
        _current_merge_range_active = false;
1000
99
        return Status::OK();
1001
99
    }
1002
181
    RowGroupReadPlan& row_group_plan = *selected_plan;
1003
181
    const int row_group_idx = row_group_plan.row_group_id;
1004
    // Dictionary probes and data-page readers share the native metadata tree. Reset the previous
1005
    // row-group merge reader before probing because dictionary-page offsets are not scan ordered.
1006
181
    file_context.reset_random_access_ranges();
1007
181
    _current_merge_range_active = false;
1008
1009
181
    const auto& row_group_metadata =
1010
181
            file_context.native_metadata->to_thrift().row_groups[row_group_idx];
1011
181
    _current_row_group_rows = row_group_metadata.num_rows;
1012
181
    DORIS_CHECK(_current_row_group_rows == row_group_plan.row_group_rows);
1013
181
    DORIS_CHECK(_current_row_group_rows > 0);
1014
181
    _current_row_group_id = row_group_idx;
1015
181
    _has_current_row_group = true;
1016
181
    DORIS_CHECK(!row_group_plan.selected_ranges.empty());
1017
181
    _current_row_group_first_row = row_group_plan.first_file_row;
1018
181
    _current_row_group_rows_read = 0;
1019
181
    _current_selected_ranges = row_group_plan.selected_ranges;
1020
181
    _current_offset_indexes = std::move(row_group_plan.offset_indexes);
1021
    // Condition Cache and split planning can narrow logical ranges without a physical OffsetIndex.
1022
    // Native readers must keep the sequential level/value cursor path valid in that case; only a
1023
    // PageIndex-derived skip plan requires the transferred indexes below.
1024
181
    for (const auto& [leaf_column_id, skip_plan] : row_group_plan.page_skip_plans) {
1025
3
        if (!_current_offset_indexes.contains(leaf_column_id)) {
1026
0
            continue;
1027
0
        }
1028
51
        for (size_t page = 0; page < skip_plan.skipped_pages.size(); ++page) {
1029
48
            if (!skip_plan.should_skip_page(page)) {
1030
24
                continue;
1031
24
            }
1032
24
            if (_page_skip_profile.skipped_pages != nullptr) {
1033
24
                COUNTER_UPDATE(_page_skip_profile.skipped_pages, 1);
1034
24
            }
1035
24
            if (_page_skip_profile.skipped_bytes != nullptr) {
1036
24
                COUNTER_UPDATE(_page_skip_profile.skipped_bytes,
1037
24
                               skip_plan.skipped_page_compressed_size(page));
1038
24
            }
1039
24
        }
1040
3
    }
1041
181
    _current_range_idx = 0;
1042
181
    _current_range_rows_read = 0;
1043
181
    _current_predicate_columns.clear();
1044
181
    _current_non_predicate_columns.clear();
1045
181
    _current_dictionary_filters.clear();
1046
181
    RETURN_IF_ERROR(prepare_current_dictionary_filters(file_context, file_schema, request,
1047
181
                                                       row_group_idx, row_group_metadata));
1048
    // Dictionary probing is complete, so the native data-page readers can now share the same
1049
    // row-group-scoped MergeRangeFileReader policy as v1. Sharing one wrapper is important: a
1050
    // separate merge reader per leaf would duplicate its 128MB scratch capacity and defeat lazy
1051
    // materialization for wide schemas.
1052
181
    const auto& thrift_metadata = file_context.native_metadata->to_thrift();
1053
181
    const auto compat = native::parquet_reader_compat(
1054
181
            thrift_metadata.__isset.created_by ? thrift_metadata.created_by : std::string {});
1055
181
    std::vector<ParquetPageCacheRange> native_ranges;
1056
181
    RETURN_IF_ERROR(detail::build_native_prefetch_ranges(
1057
181
            thrift_metadata, file_schema, request_scan_columns(request), row_group_idx,
1058
181
            file_context.native_file->size(), compat.parquet_816_padding, &native_ranges));
1059
181
    _current_merge_range_active = file_context.set_native_random_access_ranges(
1060
181
            native_ranges, detail::average_prefetch_range_size(native_ranges), _profile,
1061
181
            _merge_read_slice_size);
1062
1063
181
    for (const auto& col : request.predicate_columns) {
1064
104
        const auto local_id = col.local_id();
1065
104
        if (_current_predicate_columns.contains(local_id)) {
1066
12
            continue;
1067
12
        }
1068
92
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
1069
25
            _current_predicate_columns[local_id] = std::make_unique<RowPositionColumnReader>(
1070
25
                    _current_row_group_first_row, _scan_profile.column_reader_profile);
1071
25
            continue;
1072
25
        }
1073
67
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
1074
0
            DORIS_CHECK(_global_rowid_context.has_value());
1075
0
            _current_predicate_columns[local_id] = std::make_unique<GlobalRowIdColumnReader>(
1076
0
                    *_global_rowid_context, _current_row_group_first_row,
1077
0
                    _scan_profile.column_reader_profile);
1078
0
            continue;
1079
0
        }
1080
1081
67
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
1082
67
        const auto& column_schema = file_schema[local_id];
1083
67
        DORIS_CHECK(column_schema != nullptr);
1084
67
        std::unique_ptr<ParquetColumnReader> column_reader;
1085
67
        RETURN_IF_ERROR(NativeColumnReader::create(
1086
67
                *column_schema, &col, file_context.native_data_file(), file_context.native_metadata,
1087
67
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1088
67
                file_context.native_io_ctx, _runtime_state, file_context.native_page_cache_enabled,
1089
67
                file_context.native_page_cache_file_key,
1090
67
                _current_dictionary_filters.contains(local_id), _scan_profile.column_reader_profile,
1091
67
                &column_reader));
1092
67
        _current_predicate_columns[local_id] = std::move(column_reader);
1093
67
    }
1094
    // Start warming filter-column chunks as soon as their row group is selected. The native
1095
    // BufferedFileStreamReader later consumes the same Doris file-cache blocks; prefetch never
1096
    // changes row/column materialization order.
1097
181
    if (!_current_merge_range_active) {
1098
12
        const auto prefetch_columns = adaptive_predicate_prefetch_columns(request);
1099
12
        RETURN_IF_ERROR(prefetch_current_row_group_columns(
1100
12
                file_context, file_schema, prefetch_columns, &_current_predicate_prefetched));
1101
12
    }
1102
198
    for (const auto& col : request.non_predicate_columns) {
1103
198
        const auto local_id = col.local_id();
1104
198
        if (request.is_count_star_placeholder(col.column_id())) {
1105
1
            continue;
1106
1
        }
1107
197
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
1108
14
            _current_non_predicate_columns[local_id] = std::make_unique<RowPositionColumnReader>(
1109
14
                    _current_row_group_first_row, _scan_profile.column_reader_profile);
1110
14
            continue;
1111
14
        }
1112
183
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
1113
2
            DORIS_CHECK(_global_rowid_context.has_value());
1114
2
            _current_non_predicate_columns[local_id] = std::make_unique<GlobalRowIdColumnReader>(
1115
2
                    *_global_rowid_context, _current_row_group_first_row,
1116
2
                    _scan_profile.column_reader_profile);
1117
2
            continue;
1118
2
        }
1119
181
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
1120
181
        const auto& column_schema = file_schema[local_id];
1121
181
        DORIS_CHECK(column_schema != nullptr);
1122
181
        std::unique_ptr<ParquetColumnReader> column_reader;
1123
181
        RETURN_IF_ERROR(NativeColumnReader::create(
1124
181
                *column_schema, &col, file_context.native_data_file(), file_context.native_metadata,
1125
181
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1126
181
                file_context.native_io_ctx, _runtime_state, file_context.native_page_cache_enabled,
1127
181
                file_context.native_page_cache_file_key, false, _scan_profile.column_reader_profile,
1128
181
                &column_reader));
1129
181
        _current_non_predicate_columns[local_id] = std::move(column_reader);
1130
181
    }
1131
181
    if (!_current_merge_range_active &&
1132
181
        ((request.conjuncts.empty() && request.delete_conjuncts.empty()) ||
1133
12
         _predicate_survival_ratio >= 0.8)) {
1134
        // With no row-level filters there is no lazy-read decision to wait for, so start warming
1135
        // output chunks immediately after their readers are created. Filtered scans still defer
1136
        // this until at least one row survives the predicate phase.
1137
11
        RETURN_IF_ERROR(prefetch_current_row_group_columns(file_context, file_schema,
1138
11
                                                           physical_non_predicate_columns(request),
1139
11
                                                           &_current_non_predicate_prefetched));
1140
11
    }
1141
181
    *has_row_group = true;
1142
181
    return Status::OK();
1143
181
}
1144
1145
3
Status ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
1146
3
    DORIS_CHECK(rows >= 0);
1147
3
    if (rows == 0) {
1148
0
        return Status::OK();
1149
0
    }
1150
3
    if (_scan_profile.range_gap_skipped_rows != nullptr) {
1151
2
        COUNTER_UPDATE(_scan_profile.range_gap_skipped_rows, rows);
1152
2
    }
1153
3
    for (const auto& column_reader : _current_predicate_columns | std::views::values) {
1154
3
        RETURN_IF_ERROR(column_reader->skip(rows));
1155
3
    }
1156
    // Keep page-index/condition-cache gaps pending for lazy columns as well. For example, after a
1157
    // fully filtered [0, 32) batch and a pruned [32, 96) gap, predicate readers are at 96 while lazy
1158
    // readers remain at 0; one later skip(96) is cheaper than skip(32) followed by skip(64).
1159
3
    DORIS_CHECK(_pending_non_predicate_skip_rows <= std::numeric_limits<int64_t>::max() - rows);
1160
3
    _pending_non_predicate_skip_rows += rows;
1161
3
    _current_row_group_rows_read += rows;
1162
3
    return Status::OK();
1163
3
}
1164
1165
163
Status ParquetScanScheduler::flush_pending_non_predicate_skip_rows() {
1166
163
    if (_pending_non_predicate_skip_rows == 0) {
1167
158
        return Status::OK();
1168
158
    }
1169
5
    for (const auto& column_reader : _current_non_predicate_columns | std::views::values) {
1170
4
        RETURN_IF_ERROR(column_reader->skip(_pending_non_predicate_skip_rows));
1171
4
    }
1172
5
    _pending_non_predicate_skip_rows = 0;
1173
5
    return Status::OK();
1174
5
}
1175
1176
namespace {
1177
1178
detail::PredicateConjunctSchedule build_predicate_conjunct_schedule(
1179
152
        const format::FileScanRequest& request) {
1180
152
    std::unordered_set<size_t> predicate_block_positions;
1181
152
    predicate_block_positions.reserve(request.predicate_columns.size());
1182
152
    for (const auto& col : request.predicate_columns) {
1183
95
        const auto position_it = request.local_positions.find(col.column_id());
1184
95
        DORIS_CHECK(position_it != request.local_positions.end());
1185
95
        predicate_block_positions.insert(position_it->second.value());
1186
95
    }
1187
1188
152
    detail::PredicateConjunctSchedule schedule;
1189
152
    for (const auto& conjunct : request.conjuncts) {
1190
56
        DORIS_CHECK(conjunct != nullptr);
1191
56
        DORIS_CHECK(conjunct->root() != nullptr);
1192
56
        if (!conjunct->root()->is_safe_to_execute_on_selected_rows()) {
1193
            // Round-by-round filtering can compact later predicate columns before evaluating
1194
            // remaining expressions. Stateful functions such as random(1) and error-preserving
1195
            // functions such as assert_true() must see the same full batch they saw before this
1196
            // optimization, so any unsafe conjunct disables the per-column schedule for the batch.
1197
2
            schedule.remaining_conjuncts = request.conjuncts;
1198
2
            schedule.single_column_conjuncts.clear();
1199
2
            return schedule;
1200
2
        }
1201
54
        std::set<int> referenced_positions;
1202
54
        conjunct->root()->collect_slot_column_ids(referenced_positions);
1203
54
        if (referenced_positions.size() != 1) {
1204
4
            schedule.remaining_conjuncts.push_back(conjunct);
1205
4
            continue;
1206
4
        }
1207
50
        const auto block_position = static_cast<size_t>(*referenced_positions.begin());
1208
50
        if (!predicate_block_positions.contains(block_position)) {
1209
0
            schedule.remaining_conjuncts.push_back(conjunct);
1210
0
            continue;
1211
0
        }
1212
50
        schedule.single_column_conjuncts[block_position].push_back(conjunct);
1213
50
    }
1214
150
    return schedule;
1215
152
}
1216
1217
57
bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
1218
57
    if (conjuncts.empty()) {
1219
0
        return false;
1220
0
    }
1221
57
    return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
1222
57
        return conjunct != nullptr && conjunct->root() != nullptr &&
1223
57
               conjunct->root()->can_evaluate_dictionary_filter();
1224
57
    });
1225
57
}
1226
1227
24
bool can_evaluate_dictionary_exactly(const VExprSPtr& expr) {
1228
24
    DORIS_CHECK(expr != nullptr);
1229
24
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
1230
24
    if (compound_pred == nullptr) {
1231
21
        return expr->can_evaluate_dictionary_filter();
1232
21
    }
1233
3
    if (compound_pred->op() != TExprOpcode::COMPOUND_AND &&
1234
3
        compound_pred->op() != TExprOpcode::COMPOUND_OR) {
1235
0
        return false;
1236
0
    }
1237
3
    return !expr->children().empty() &&
1238
6
           std::ranges::all_of(expr->children(), [](const auto& child) {
1239
6
               return can_evaluate_dictionary_exactly(child);
1240
6
           });
1241
3
}
1242
1243
void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context, const VExprSPtr& expr,
1244
18
                                       DictionaryResidualConjuncts* residual_conjuncts) {
1245
18
    DORIS_CHECK(owner_context != nullptr);
1246
18
    DORIS_CHECK(expr != nullptr);
1247
18
    DORIS_CHECK(residual_conjuncts != nullptr);
1248
1249
18
    if (can_evaluate_dictionary_exactly(expr)) {
1250
12
        return;
1251
12
    }
1252
1253
    // VCompoundPred dictionary evaluation is a conservative prefilter for AND when only some
1254
    // children are dictionary-aware. Split AND so exact dictionary children are not executed again
1255
    // on materialized rows. Do not split a non-exact OR: its branches cannot be evaluated
1256
    // independently after a dictionary prefilter without changing the original boolean semantics.
1257
6
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
1258
6
    if (compound_pred != nullptr && compound_pred->op() == TExprOpcode::COMPOUND_AND) {
1259
6
        for (const auto& child : expr->children()) {
1260
6
            collect_dictionary_residual_exprs(owner_context, child, residual_conjuncts);
1261
6
        }
1262
3
        return;
1263
3
    }
1264
1265
3
    residual_conjuncts->emplace_back(owner_context, expr);
1266
3
}
1267
1268
DictionaryResidualConjuncts build_dictionary_residual_conjuncts(
1269
12
        const VExprContextSPtrs& conjuncts) {
1270
12
    DictionaryResidualConjuncts residual_conjuncts;
1271
12
    for (const auto& conjunct : conjuncts) {
1272
12
        DORIS_CHECK(conjunct != nullptr);
1273
12
        collect_dictionary_residual_exprs(conjunct, conjunct->root(), &residual_conjuncts);
1274
12
    }
1275
12
    return residual_conjuncts;
1276
12
}
1277
1278
54
uint16_t count_selected_rows(const IColumn::Filter& filter) {
1279
54
    uint16_t selected_rows = 0;
1280
9.69k
    for (const auto value : filter) {
1281
9.69k
        selected_rows += value != 0;
1282
9.69k
    }
1283
54
    return selected_rows;
1284
54
}
1285
1286
IColumn::Filter build_dictionary_entry_filter(size_t block_position,
1287
                                              const ParquetColumnSchema& column_schema,
1288
                                              const VExprContextSPtrs& conjuncts,
1289
12
                                              const IColumn& dictionary) {
1290
12
    IColumn::Filter dictionary_filter(dictionary.size(), 1);
1291
12
    DictionaryEvalContext ctx;
1292
12
    auto& slot = ctx.slots
1293
12
                         .emplace(static_cast<int>(block_position),
1294
12
                                  DictionaryEvalContext::SlotDictionary {
1295
12
                                          .data_type = column_schema.type, .values = {}})
1296
12
                         .first->second;
1297
12
    slot.values.reserve(1);
1298
57
    for (size_t dictionary_id = 0; dictionary_id < dictionary.size(); ++dictionary_id) {
1299
45
        Field value;
1300
45
        dictionary.get(dictionary_id, value);
1301
45
        slot.values.clear();
1302
45
        slot.values.push_back(std::move(value));
1303
45
        dictionary_filter[dictionary_id] = VExprContext::evaluate_dictionary_filter(
1304
45
                                                   conjuncts, ctx) == ZoneMapFilterResult::kNoMatch
1305
45
                                                   ? 0
1306
45
                                                   : 1;
1307
45
    }
1308
12
    return dictionary_filter;
1309
12
}
1310
1311
} // namespace
1312
1313
Status ParquetScanScheduler::prepare_current_dictionary_filters(
1314
        ParquetFileContext& file_context,
1315
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1316
        const format::FileScanRequest& request, int row_group_idx,
1317
181
        const tparquet::RowGroup& row_group_metadata) {
1318
181
    _current_dictionary_filters.clear();
1319
181
    _current_dictionary_residual_conjuncts.clear();
1320
181
    if (request.conjuncts.empty()) {
1321
122
        return Status::OK();
1322
122
    }
1323
59
    detail::PredicateConjunctSchedule schedule;
1324
59
    {
1325
59
        SCOPED_TIMER(_scan_profile.dict_filter_expr_rewrite_time);
1326
59
        schedule = predicate_conjunct_schedule(request);
1327
59
    }
1328
59
    if (schedule.single_column_conjuncts.empty()) {
1329
5
        return Status::OK();
1330
5
    }
1331
1332
54
    SCOPED_TIMER(_scan_profile.dict_filter_rewrite_time);
1333
59
    for (const auto& col : request.predicate_columns) {
1334
59
        const auto local_id = col.local_id();
1335
59
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size())) {
1336
1
            continue;
1337
1
        }
1338
58
        const auto position_it = request.local_positions.find(col.column_id());
1339
58
        DORIS_CHECK(position_it != request.local_positions.end());
1340
58
        const auto block_position = static_cast<size_t>(position_it->second.value());
1341
58
        const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
1342
58
        if (conjunct_it == schedule.single_column_conjuncts.end() ||
1343
58
            !can_evaluate_all_with_dictionary(conjunct_it->second)) {
1344
46
            continue;
1345
46
        }
1346
12
        update_counter_if_not_null(_scan_profile.dict_filter_candidate_columns, 1);
1347
1348
        // This optimization is deliberately limited to single-column predicates with a dictionary
1349
        // evaluable part. Mixed AND predicates are split so dictionary-covered children run as a
1350
        // dict-id prefilter and residual children keep the normal row-level expression path.
1351
12
        const auto& column_schema = file_schema[local_id];
1352
12
        DORIS_CHECK(column_schema != nullptr);
1353
12
        if (column_schema->leaf_column_id < 0 ||
1354
12
            column_schema->leaf_column_id >= static_cast<int>(row_group_metadata.columns.size())) {
1355
0
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1356
0
            continue;
1357
0
        }
1358
12
        const auto& column_chunk = row_group_metadata.columns[column_schema->leaf_column_id];
1359
12
        if (!column_chunk.__isset.meta_data ||
1360
12
            !supports_row_level_dictionary_filter(*column_schema, column_chunk.meta_data)) {
1361
0
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1362
0
            continue;
1363
0
        }
1364
1365
12
        std::unique_ptr<ParquetColumnReader> column_reader;
1366
12
        RETURN_IF_ERROR(NativeColumnReader::create(
1367
12
                *column_schema, &col, file_context.native_file, file_context.native_metadata,
1368
12
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1369
12
                file_context.native_io_ctx, _runtime_state, file_context.native_page_cache_enabled,
1370
12
                file_context.native_page_cache_file_key, true, _scan_profile.column_reader_profile,
1371
12
                &column_reader));
1372
12
        MutableColumnPtr dictionary_values;
1373
12
        {
1374
12
            SCOPED_TIMER(_scan_profile.dict_filter_read_dict_time);
1375
12
            auto dictionary_result = column_reader->dictionary_values();
1376
12
            if (!dictionary_result.has_value()) {
1377
0
                update_counter_if_not_null(_scan_profile.dict_filter_read_failures, 1);
1378
                // Dictionary filtering is optional: a probe failure must not reject a file that
1379
                // the normal native read path can still decode.
1380
0
                continue;
1381
0
            }
1382
12
            dictionary_values = std::move(dictionary_result).value();
1383
12
        }
1384
1385
        // Build a safe dictionary prefilter from the dictionary-filter interface instead of
1386
        // executing the row expression on a temporary dictionary block. For compound AND,
1387
        // VCompoundPred intentionally evaluates only dictionary-capable children, so residual
1388
        // predicates still run later on surviving rows.
1389
0
        IColumn::Filter dictionary_filter;
1390
12
        DictionaryResidualConjuncts residual_conjuncts;
1391
12
        {
1392
12
            SCOPED_TIMER(_scan_profile.dict_filter_build_time);
1393
12
            dictionary_filter = build_dictionary_entry_filter(
1394
12
                    block_position, *column_schema, conjunct_it->second, *dictionary_values);
1395
12
            residual_conjuncts = build_dictionary_residual_conjuncts(conjunct_it->second);
1396
12
        }
1397
1398
        // The bitmap is keyed by Parquet dictionary id. Later data-page reads evaluate the
1399
        // predicate with an integer lookup and only materialize STRING values for surviving rows.
1400
12
        _current_dictionary_filters.emplace(local_id, std::move(dictionary_filter));
1401
12
        _current_dictionary_residual_conjuncts.emplace(local_id, std::move(residual_conjuncts));
1402
12
        _current_predicate_columns.emplace(local_id, std::move(column_reader));
1403
12
        update_counter_if_not_null(_scan_profile.dict_filter_columns, 1);
1404
12
    }
1405
54
    return Status::OK();
1406
54
}
1407
1408
Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
1409
                                                 const format::FileScanRequest& request,
1410
                                                 Block* file_block, SelectionVector* selection,
1411
                                                 uint16_t* selected_rows,
1412
                                                 int64_t* conjunct_filtered_rows,
1413
198
                                                 bool* predicate_columns_filtered) {
1414
198
    DORIS_CHECK(predicate_columns_filtered != nullptr);
1415
198
    *predicate_columns_filtered = false;
1416
198
    if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
1417
116
        selection->resize(static_cast<size_t>(batch_rows));
1418
116
    }
1419
198
    const auto& schedule = predicate_conjunct_schedule(request);
1420
198
    std::unordered_set<size_t> residual_predicate_positions;
1421
396
    auto remember_residual_positions = [&](const VExprContextSPtrs& conjuncts) {
1422
396
        for (const auto& conjunct : conjuncts) {
1423
43
            std::set<int> positions;
1424
43
            conjunct->root()->collect_slot_column_ids(positions);
1425
43
            for (const int position : positions) {
1426
40
                if (position >= 0) {
1427
40
                    residual_predicate_positions.insert(cast_set<size_t>(position));
1428
40
                }
1429
40
            }
1430
43
        }
1431
396
    };
1432
198
    remember_residual_positions(schedule.remaining_conjuncts);
1433
198
    remember_residual_positions(request.delete_conjuncts);
1434
198
    const size_t predicate_batch_sequence = _predicate_batch_sequence++;
1435
198
    const bool can_read_predicate_columns_round_by_round =
1436
198
            !schedule.single_column_conjuncts.empty();
1437
198
    auto& read_column_positions = _read_column_positions_scratch;
1438
198
    read_column_positions.clear();
1439
198
    read_column_positions.reserve(request.predicate_columns.size());
1440
198
    for (auto& rows : _predicate_column_selection_scratch | std::views::values) {
1441
32
        rows.clear();
1442
32
    }
1443
1444
198
    auto remember_column_selection = [&](uint32_t position) {
1445
177
        auto& rows = _predicate_column_selection_scratch[position];
1446
177
        rows.resize(*selected_rows);
1447
78.9k
        for (uint16_t row = 0; row < *selected_rows; ++row) {
1448
            // SelectionVector and the scanner batch contract both bound row ordinals to uint16_t;
1449
            // keep the checked conversion explicit when persisting the coordinate mapping.
1450
78.7k
            rows[row] = cast_set<uint16_t>(selection->get_index(row));
1451
78.7k
        }
1452
177
    };
1453
1454
198
    auto compact_predicate_columns = [&](bool discard_predicate_only_payload) -> Status {
1455
79
        bool compacted = false;
1456
79
        int64_t compacted_bytes = 0;
1457
84
        for (const uint32_t position : read_column_positions) {
1458
84
            auto& source_rows = _predicate_column_selection_scratch[position];
1459
84
            const auto& old_column = file_block->get_by_position(position).column;
1460
84
            if (old_column->size() != source_rows.size()) {
1461
0
                return Status::Corruption(
1462
0
                        "Predicate column {} has {} values but {} remembered source rows", position,
1463
0
                        old_column->size(), source_rows.size());
1464
0
            }
1465
84
            bool predicate_only = false;
1466
84
            if (discard_predicate_only_payload) {
1467
80
                predicate_only = std::ranges::any_of(
1468
80
                        request.predicate_only_columns, [&](format::LocalColumnId local_id) {
1469
4
                            const auto position_it = request.local_positions.find(local_id);
1470
4
                            return position_it != request.local_positions.end() &&
1471
4
                                   position_it->second.value() == position;
1472
4
                        });
1473
80
            }
1474
84
            if (predicate_only) {
1475
3
                auto placeholder = old_column->clone_empty();
1476
                // Hidden predicate values are dead after the last filter, but every file-block
1477
                // column must retain the selected row count until TableReader drops hidden slots.
1478
3
                placeholder->insert_many_defaults(*selected_rows);
1479
3
                file_block->replace_by_position(position, std::move(placeholder));
1480
3
                remember_column_selection(position);
1481
3
                continue;
1482
3
            }
1483
81
            bool already_compact = source_rows.size() == *selected_rows &&
1484
81
                                   old_column->size() == static_cast<size_t>(*selected_rows);
1485
2.42k
            for (uint16_t row = 0; already_compact && row < *selected_rows; ++row) {
1486
2.34k
                already_compact = source_rows[row] == selection->get_index(row);
1487
2.34k
            }
1488
81
            if (already_compact) {
1489
32
                continue;
1490
32
            }
1491
49
            auto& filter = _predicate_compaction_filter_scratch;
1492
            // resize_fill() preserves bytes when the next predicate column is smaller. Clear the
1493
            // whole reusable mask so survivors from an earlier coordinate space cannot reappear.
1494
49
            filter.resize(source_rows.size());
1495
49
            std::ranges::fill(filter, 0);
1496
49
            size_t source_idx = 0;
1497
49
            uint16_t selected_idx = 0;
1498
7.36k
            while (source_idx < source_rows.size() && selected_idx < *selected_rows) {
1499
7.31k
                const auto source_row = source_rows[source_idx];
1500
7.31k
                const auto selected_row = selection->get_index(selected_idx);
1501
7.31k
                if (source_row < selected_row) {
1502
5.22k
                    ++source_idx;
1503
5.22k
                    continue;
1504
5.22k
                }
1505
2.08k
                DORIS_CHECK_EQ(source_row, selected_row);
1506
2.08k
                filter[source_idx++] = 1;
1507
2.08k
                ++selected_idx;
1508
2.08k
            }
1509
49
            DORIS_CHECK_EQ(selected_idx, *selected_rows);
1510
49
            compacted_bytes += static_cast<int64_t>(old_column->byte_size());
1511
49
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
1512
49
                    position, old_column->filter(filter, *selected_rows)));
1513
49
            remember_column_selection(position);
1514
49
            compacted = true;
1515
49
        }
1516
79
        if (compacted) {
1517
47
            update_counter_if_not_null(_scan_profile.predicate_compaction_bytes, compacted_bytes);
1518
47
            update_counter_if_not_null(_scan_profile.predicate_compaction_count, 1);
1519
47
        }
1520
        // The output path must not apply a batch-coordinate filter to columns that now use compact
1521
        // coordinates. The loop above establishes this invariant even when no bytes moved because
1522
        // every column was already aligned.
1523
79
        *predicate_columns_filtered = !read_column_positions.empty();
1524
79
        return Status::OK();
1525
79
    };
1526
1527
198
    auto read_predicate_column =
1528
198
            [&](ParquetColumnReader* column_reader, size_t block_position, ColumnId local_id,
1529
198
                const VExprContextSPtrs* single_column_conjuncts, bool* used_dictionary_filter,
1530
198
                bool* used_plain_filter) -> Status {
1531
125
        DORIS_CHECK(used_dictionary_filter != nullptr);
1532
125
        DORIS_CHECK(used_plain_filter != nullptr);
1533
125
        *used_dictionary_filter = false;
1534
125
        *used_plain_filter = false;
1535
125
        DCHECK(remove_nullable(column_reader->type())
1536
0
                       ->equals(*remove_nullable(file_block->get_by_position(block_position).type)))
1537
0
                << column_reader->type()->get_name() << " "
1538
0
                << file_block->get_by_position(block_position).type->get_name() << " "
1539
0
                << column_reader->name() << " " << file_block->get_by_position(block_position).name;
1540
125
        auto column = file_block->get_by_position(block_position).column->assert_mutable();
1541
125
        SCOPED_TIMER(_scan_profile.column_read_time);
1542
125
        const auto dictionary_filter_it = _current_dictionary_filters.find(local_id);
1543
125
        if (dictionary_filter_it != _current_dictionary_filters.end()) {
1544
12
            const uint16_t selected_rows_before = *selected_rows;
1545
12
            IColumn::Filter compact_filter;
1546
12
            bool used_filter = false;
1547
12
            RETURN_IF_ERROR(column_reader->select_with_dictionary_filter(
1548
12
                    *selection, *selected_rows, batch_rows, dictionary_filter_it->second, column,
1549
12
                    &compact_filter, &used_filter));
1550
12
            if (used_filter) {
1551
12
                DORIS_CHECK(compact_filter.size() == selected_rows_before);
1552
12
                const uint16_t new_selected_rows = count_selected_rows(compact_filter);
1553
12
                const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
1554
12
                                           static_cast<int64_t>(new_selected_rows);
1555
12
                if (conjunct_filtered_rows != nullptr) {
1556
12
                    *conjunct_filtered_rows += filtered_rows;
1557
12
                }
1558
12
                update_counter_if_not_null(_scan_profile.rows_filtered_by_dict_filter,
1559
12
                                           filtered_rows);
1560
12
                if (new_selected_rows != selected_rows_before) {
1561
                    // The dictionary reader already appended only survivors for this column. Keep
1562
                    // older predicate columns in their original coordinate spaces and compact all
1563
                    // of them once at the expression/output boundary below.
1564
8
                    *selected_rows = apply_compact_filter_to_selection(compact_filter, selection,
1565
8
                                                                       selected_rows_before);
1566
8
                }
1567
12
                file_block->replace_by_position(block_position, std::move(column));
1568
12
                read_column_positions.push_back(cast_set<uint32_t>(block_position));
1569
12
                remember_column_selection(cast_set<uint32_t>(block_position));
1570
12
                *used_dictionary_filter = true;
1571
12
                return Status::OK();
1572
12
            }
1573
12
        }
1574
1575
113
        if (single_column_conjuncts != nullptr &&
1576
113
            !residual_predicate_positions.contains(block_position) &&
1577
113
            request.is_predicate_only(format::LocalColumnId(cast_set<int32_t>(local_id)))) {
1578
2
            VExprSPtrs direct_conjuncts;
1579
2
            direct_conjuncts.reserve(single_column_conjuncts->size());
1580
2
            std::ranges::transform(*single_column_conjuncts, std::back_inserter(direct_conjuncts),
1581
2
                                   [](const auto& context) { return context->root(); });
1582
2
            if (!direct_conjuncts.empty()) {
1583
2
                const uint16_t selected_rows_before = *selected_rows;
1584
2
                IColumn::Filter compact_filter;
1585
2
                bool used_filter = false;
1586
2
                RETURN_IF_ERROR(column_reader->select_with_plain_filter(
1587
2
                        *selection, *selected_rows, batch_rows, direct_conjuncts,
1588
2
                        cast_set<int>(block_position), &compact_filter, &used_filter));
1589
2
                if (used_filter) {
1590
1
                    DORIS_CHECK_EQ(compact_filter.size(), selected_rows_before);
1591
1
                    update_counter_if_not_null(_scan_profile.plain_predicate_direct_batches, 1);
1592
1
                    update_counter_if_not_null(_scan_profile.plain_predicate_direct_rows,
1593
1
                                               selected_rows_before);
1594
1
                    const uint16_t new_selected_rows = count_selected_rows(compact_filter);
1595
1
                    const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
1596
1
                                               static_cast<int64_t>(new_selected_rows);
1597
1
                    if (conjunct_filtered_rows != nullptr) {
1598
1
                        *conjunct_filtered_rows += filtered_rows;
1599
1
                    }
1600
1
                    if (new_selected_rows != selected_rows_before) {
1601
1
                        *selected_rows = apply_compact_filter_to_selection(
1602
1
                                compact_filter, selection, selected_rows_before);
1603
1
                    }
1604
                    // This slot is absent from every residual/delete conjunct, so no later
1605
                    // expression can observe its payload. Keep only the block row-shape contract.
1606
1
                    auto placeholder = column->clone_empty();
1607
1
                    placeholder->insert_many_defaults(*selected_rows);
1608
1
                    file_block->replace_by_position(block_position, std::move(placeholder));
1609
1
                    read_column_positions.push_back(cast_set<uint32_t>(block_position));
1610
1
                    remember_column_selection(cast_set<uint32_t>(block_position));
1611
1
                    *predicate_columns_filtered = true;
1612
1
                    *used_plain_filter = true;
1613
1
                    return Status::OK();
1614
1
                }
1615
2
            }
1616
2
        }
1617
1618
112
        if (*selected_rows == batch_rows) {
1619
109
            int64_t column_rows = 0;
1620
109
            RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
1621
109
            if (column_rows != batch_rows) {
1622
0
                return Status::Corruption(
1623
0
                        "Parquet filter column {} returned {} rows, expected {} rows",
1624
0
                        column_reader->name(), column_rows, batch_rows);
1625
0
            }
1626
109
        } else {
1627
3
            [[maybe_unused]] auto old_size = column->size();
1628
3
            RETURN_IF_ERROR(column_reader->select(*selection, *selected_rows, batch_rows, column));
1629
3
            if (column->size() != old_size + *selected_rows) {
1630
0
                return Status::Corruption(
1631
0
                        "Parquet selected filter column {} returned {} rows, expected {} rows",
1632
0
                        column_reader->name(), column->size(), old_size + *selected_rows);
1633
0
            }
1634
3
            *predicate_columns_filtered = true;
1635
3
        }
1636
112
        file_block->replace_by_position(block_position, std::move(column));
1637
112
        read_column_positions.push_back(cast_set<uint32_t>(block_position));
1638
112
        remember_column_selection(cast_set<uint32_t>(block_position));
1639
112
        return Status::OK();
1640
112
    };
1641
1642
198
    auto execute_scheduled_conjuncts = [&](const VExprContextSPtrs& conjuncts) -> Status {
1643
142
        if (conjuncts.empty() || *selected_rows == 0) {
1644
76
            return Status::OK();
1645
76
        }
1646
66
        const uint16_t selected_rows_before = *selected_rows;
1647
66
        IColumn::Filter compact_filter;
1648
66
        bool can_filter_all = false;
1649
66
        RETURN_IF_ERROR(execute_compact_filter_conjuncts(
1650
66
                conjuncts, selected_rows_before, file_block, &compact_filter, &can_filter_all));
1651
66
        if (can_filter_all) {
1652
28
            compact_filter.resize_fill(selected_rows_before, 0);
1653
28
        }
1654
66
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
1655
66
        if (conjunct_filtered_rows != nullptr) {
1656
66
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
1657
66
                                       static_cast<int64_t>(new_selected_rows);
1658
66
        }
1659
66
        if (new_selected_rows != selected_rows_before) {
1660
45
            *selected_rows = can_filter_all
1661
45
                                     ? 0
1662
45
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1663
17
                                                                         selected_rows_before);
1664
45
        }
1665
66
        return Status::OK();
1666
66
    };
1667
1668
198
    auto execute_scheduled_dictionary_residual_conjuncts =
1669
198
            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
1670
12
        if (conjuncts.empty() || *selected_rows == 0) {
1671
9
            return Status::OK();
1672
9
        }
1673
3
        const uint16_t selected_rows_before = *selected_rows;
1674
3
        IColumn::Filter compact_filter;
1675
3
        bool can_filter_all = false;
1676
3
        RETURN_IF_ERROR(execute_compact_dictionary_residual_conjuncts(
1677
3
                conjuncts, selected_rows_before, file_block, &compact_filter, &can_filter_all));
1678
3
        if (can_filter_all) {
1679
1
            compact_filter.resize_fill(selected_rows_before, 0);
1680
1
        }
1681
3
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
1682
3
        if (conjunct_filtered_rows != nullptr) {
1683
3
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
1684
3
                                       static_cast<int64_t>(new_selected_rows);
1685
3
        }
1686
3
        if (new_selected_rows != selected_rows_before) {
1687
3
            *selected_rows = can_filter_all
1688
3
                                     ? 0
1689
3
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1690
2
                                                                         selected_rows_before);
1691
3
        }
1692
3
        return Status::OK();
1693
3
    };
1694
1695
198
    auto execute_scheduled_conjuncts_with_profile =
1696
198
            [&](const VExprContextSPtrs& conjuncts) -> Status {
1697
142
        if (_scan_profile.predicate_filter_time == nullptr) {
1698
55
            return execute_scheduled_conjuncts(conjuncts);
1699
55
        }
1700
87
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1701
87
        return execute_scheduled_conjuncts(conjuncts);
1702
142
    };
1703
1704
198
    auto execute_scheduled_dictionary_residual_conjuncts_with_profile =
1705
198
            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
1706
12
        if (_scan_profile.predicate_filter_time == nullptr) {
1707
7
            return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
1708
7
        }
1709
5
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1710
5
        return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
1711
12
    };
1712
1713
198
    auto execute_scheduled_delete_conjuncts = [&]() -> Status {
1714
77
        if (request.delete_conjuncts.empty() || *selected_rows == 0) {
1715
76
            return Status::OK();
1716
76
        }
1717
1
        const uint16_t selected_rows_before = *selected_rows;
1718
1
        IColumn::Filter compact_filter;
1719
1
        bool can_filter_all = false;
1720
1
        RETURN_IF_ERROR(execute_compact_delete_conjuncts(request.delete_conjuncts,
1721
1
                                                         selected_rows_before, file_block,
1722
1
                                                         &compact_filter, &can_filter_all));
1723
1
        if (can_filter_all) {
1724
0
            compact_filter.resize_fill(selected_rows_before, 0);
1725
0
        }
1726
1
        if (can_filter_all || count_selected_rows(compact_filter) != selected_rows_before) {
1727
1
            *selected_rows = can_filter_all
1728
1
                                     ? 0
1729
1
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1730
1
                                                                         selected_rows_before);
1731
1
        }
1732
1
        return Status::OK();
1733
1
    };
1734
1735
198
    auto read_all_predicate_columns = [&]() -> Status {
1736
121
        for (const auto& [fid, column_reader] : _current_predicate_columns) {
1737
45
            auto position_it = request.local_positions.find(format::LocalColumnId(fid));
1738
45
            DORIS_CHECK(position_it != request.local_positions.end());
1739
45
            bool used_dictionary_filter = false;
1740
45
            bool used_plain_filter = false;
1741
45
            RETURN_IF_ERROR(read_predicate_column(column_reader.get(), position_it->second.value(),
1742
45
                                                  fid, nullptr, &used_dictionary_filter,
1743
45
                                                  &used_plain_filter));
1744
45
        }
1745
121
        return Status::OK();
1746
121
    };
1747
1748
198
    if (!can_read_predicate_columns_round_by_round) {
1749
121
        RETURN_IF_ERROR(read_all_predicate_columns());
1750
121
        if (_scan_profile.predicate_filter_time == nullptr) {
1751
47
            return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
1752
47
                                         conjunct_filtered_rows);
1753
47
        }
1754
74
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1755
74
        return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
1756
74
                                     conjunct_filtered_rows);
1757
121
    }
1758
1759
77
    auto read_round_by_round = [&]() -> Status {
1760
        // Single-column conjuncts can be evaluated immediately after their column is read. Once
1761
        // selection shrinks, later predicate columns use ParquetColumnReader::select() so the
1762
        // reader skips rows already rejected by earlier predicates instead of materializing them.
1763
77
        _ordered_predicate_positions_scratch = detail::order_adaptive_predicates(
1764
77
                _predicate_positions_scratch, _predicate_runtime_stats);
1765
77
        const auto& ordered_positions = _ordered_predicate_positions_scratch;
1766
128
        for (size_t order_idx = 0; order_idx < ordered_positions.size(); ++order_idx) {
1767
80
            const size_t position = ordered_positions[order_idx];
1768
80
            const size_t idx = _predicate_indices_by_position_scratch.at(position);
1769
80
            const auto& col = request.predicate_columns[idx];
1770
80
            const auto fid = col.local_id();
1771
80
            auto reader_it = _current_predicate_columns.find(fid);
1772
80
            DORIS_CHECK(reader_it != _current_predicate_columns.end());
1773
80
            auto position_it = request.local_positions.find(col.column_id());
1774
80
            DORIS_CHECK(position_it != request.local_positions.end());
1775
80
            const auto block_position = position_it->second.value();
1776
80
            const uint16_t rows_before = *selected_rows;
1777
80
            auto& stats = _predicate_runtime_stats[position];
1778
80
            const bool sample = detail::should_sample_adaptive_predicate(stats.samples,
1779
80
                                                                         predicate_batch_sequence);
1780
80
            const int64_t start_ns = sample ? MonotonicNanos() : 0;
1781
80
            bool used_dictionary_filter = false;
1782
80
            bool used_plain_filter = false;
1783
80
            const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
1784
80
            const VExprContextSPtrs* column_conjuncts =
1785
80
                    conjunct_it == schedule.single_column_conjuncts.end() ? nullptr
1786
80
                                                                          : &conjunct_it->second;
1787
80
            RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(), block_position, fid,
1788
80
                                                  column_conjuncts, &used_dictionary_filter,
1789
80
                                                  &used_plain_filter));
1790
80
            if (*selected_rows != 0 && conjunct_it != schedule.single_column_conjuncts.end()) {
1791
78
                if (used_dictionary_filter) {
1792
12
                    const auto residual_it = _current_dictionary_residual_conjuncts.find(fid);
1793
12
                    DORIS_CHECK(residual_it != _current_dictionary_residual_conjuncts.end());
1794
12
                    RETURN_IF_ERROR(execute_scheduled_dictionary_residual_conjuncts_with_profile(
1795
12
                            residual_it->second));
1796
66
                } else if (!used_plain_filter) {
1797
65
                    RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));
1798
65
                }
1799
78
            }
1800
80
            if (sample) {
1801
69
                const double cost_per_row = static_cast<double>(MonotonicNanos() - start_ns) /
1802
69
                                            std::max<uint16_t>(rows_before, 1);
1803
69
                const double survival =
1804
69
                        static_cast<double>(*selected_rows) / std::max<uint16_t>(rows_before, 1);
1805
69
                constexpr double ADAPTIVE_ALPHA = 0.25;
1806
69
                if (stats.samples == 0) {
1807
48
                    stats.cost_per_input_row_ns = cost_per_row;
1808
48
                    stats.survival_ratio = survival;
1809
48
                } else {
1810
21
                    stats.cost_per_input_row_ns =
1811
21
                            ADAPTIVE_ALPHA * cost_per_row +
1812
21
                            (1 - ADAPTIVE_ALPHA) * stats.cost_per_input_row_ns;
1813
21
                    stats.survival_ratio =
1814
21
                            ADAPTIVE_ALPHA * survival + (1 - ADAPTIVE_ALPHA) * stats.survival_ratio;
1815
21
                }
1816
69
                ++stats.samples;
1817
69
            }
1818
80
            if (*selected_rows != 0) {
1819
51
                continue;
1820
51
            }
1821
29
            for (size_t remaining_order_idx = order_idx + 1;
1822
31
                 remaining_order_idx < ordered_positions.size(); ++remaining_order_idx) {
1823
2
                const size_t remaining_idx = _predicate_indices_by_position_scratch.at(
1824
2
                        ordered_positions[remaining_order_idx]);
1825
2
                const auto remaining_fid = request.predicate_columns[remaining_idx].local_id();
1826
2
                auto remaining_reader_it = _current_predicate_columns.find(remaining_fid);
1827
2
                DORIS_CHECK(remaining_reader_it != _current_predicate_columns.end());
1828
2
                RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
1829
2
            }
1830
29
            return Status::OK();
1831
29
        }
1832
48
        return Status::OK();
1833
77
    };
1834
1835
77
    auto compact_predicate_columns_with_profile =
1836
79
            [&](bool discard_predicate_only_payload) -> Status {
1837
79
        const int64_t start_ns = MonotonicNanos();
1838
79
        auto status = compact_predicate_columns(discard_predicate_only_payload);
1839
79
        update_counter_if_not_null(_scan_profile.predicate_compaction_time,
1840
79
                                   MonotonicNanos() - start_ns);
1841
79
        return status;
1842
79
    };
1843
1844
77
    RETURN_IF_ERROR(read_round_by_round());
1845
    // Single-column expressions only touch the just-read column, so earlier columns can retain
1846
    // their own row mappings. Compact only when a later expression needs a shared coordinate
1847
    // space; otherwise the final boundary can discard hidden predicate payloads without scanning
1848
    // them again.
1849
77
    if (!schedule.remaining_conjuncts.empty()) {
1850
1
        RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
1851
1
    }
1852
77
    RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(schedule.remaining_conjuncts));
1853
77
    if (!request.delete_conjuncts.empty()) {
1854
1
        RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
1855
1
    }
1856
77
    if (_scan_profile.predicate_filter_time == nullptr) {
1857
31
        RETURN_IF_ERROR(execute_scheduled_delete_conjuncts());
1858
46
    } else {
1859
46
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1860
46
        RETURN_IF_ERROR(execute_scheduled_delete_conjuncts());
1861
46
    }
1862
77
    return compact_predicate_columns_with_profile(true);
1863
77
}
1864
1865
Status ParquetScanScheduler::prefetch_current_row_group_columns(
1866
        ParquetFileContext& file_context,
1867
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1868
23
        const std::vector<format::LocalColumnIndex>& scan_columns, bool* prefetched) {
1869
23
    DORIS_CHECK(prefetched != nullptr);
1870
23
    if (_current_merge_range_active || *prefetched || scan_columns.empty() ||
1871
23
        _current_row_group_id < 0 || file_context.native_metadata == nullptr) {
1872
22
        return Status::OK();
1873
22
    }
1874
1
    *prefetched = true;
1875
    // The scanner request separates predicate and non-predicate columns so Parquet can read
1876
    // predicate columns first and lazily materialize the rest. Keep the same contract for
1877
    // prefetch: callers decide which side to warm, and this helper only translates that selected
1878
    // projection into physical column-chunk byte ranges for the current row group.
1879
1
    const auto& metadata = file_context.native_metadata->to_thrift();
1880
1
    const auto compat = native::parquet_reader_compat(
1881
1
            metadata.__isset.created_by ? metadata.created_by : std::string {});
1882
1
    std::vector<ParquetPageCacheRange> ranges;
1883
1
    RETURN_IF_ERROR(detail::build_native_prefetch_ranges(
1884
1
            metadata, file_schema, scan_columns, _current_row_group_id,
1885
1
            file_context.native_file->size(), compat.parquet_816_padding, &ranges));
1886
1
    file_context.prefetch_ranges(ranges, nullptr);
1887
1
    return Status::OK();
1888
1
}
1889
1890
Status ParquetScanScheduler::read_current_row_group_batch(
1891
        ParquetFileContext& file_context,
1892
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, int64_t batch_rows,
1893
        const format::FileScanRequest& request, int64_t batch_first_file_row, Block* file_block,
1894
209
        size_t* rows) {
1895
    // Reader statistics are cumulative plain integers. Publishing their delta recursively for
1896
    // every tiny batch is measurable on wide/nested scans, so flush periodically and force the
1897
    // tail at row-group reset/close.
1898
209
    Defer profile_flush {[this, batch_rows]() {
1899
209
        if (finish_current_reader_batch_profiles() &&
1900
209
            _scan_profile.column_reader_profile.page_crossing_batches != nullptr) {
1901
3
            COUNTER_UPDATE(_scan_profile.column_reader_profile.page_crossing_batches, 1);
1902
3
        }
1903
209
        const bool finishes_row_group = _current_range_idx + 1 == _current_selected_ranges.size() &&
1904
209
                                        _current_range_rows_read + batch_rows ==
1905
209
                                                _current_selected_ranges[_current_range_idx].length;
1906
209
        if (++_batches_since_profile_flush >= PROFILE_FLUSH_BATCH_INTERVAL || finishes_row_group) {
1907
182
            flush_current_reader_profiles();
1908
182
            _batches_since_profile_flush = 0;
1909
182
        }
1910
209
    }};
1911
209
    if (_scan_profile.total_batches != nullptr) {
1912
120
        COUNTER_UPDATE(_scan_profile.total_batches, 1);
1913
120
    }
1914
209
    if (_scan_profile.raw_rows_read != nullptr) {
1915
120
        COUNTER_UPDATE(_scan_profile.raw_rows_read, batch_rows);
1916
120
    }
1917
209
    _raw_rows_read += batch_rows;
1918
209
    if (_current_predicate_columns.empty() && _current_non_predicate_columns.empty()) {
1919
11
        *rows = static_cast<size_t>(batch_rows);
1920
11
        materialize_count_star_placeholders(request, *rows, file_block);
1921
11
        if (_scan_profile.selected_rows != nullptr) {
1922
0
            COUNTER_UPDATE(_scan_profile.selected_rows, batch_rows);
1923
0
        }
1924
11
        return Status::OK();
1925
11
    }
1926
198
    auto& selection = _selection;
1927
198
    DORIS_CHECK(batch_rows <= std::numeric_limits<uint16_t>::max());
1928
198
    uint16_t selected_rows = static_cast<uint16_t>(batch_rows);
1929
198
    int64_t conjunct_filtered_rows = 0;
1930
198
    bool predicate_columns_filtered = false;
1931
198
    RETURN_IF_ERROR(read_filter_columns(batch_rows, request, file_block, &selection, &selected_rows,
1932
198
                                        &conjunct_filtered_rows, &predicate_columns_filtered));
1933
198
    _predicate_filtered_rows += conjunct_filtered_rows;
1934
198
    mark_condition_cache_granules(selection, selected_rows, batch_first_file_row);
1935
1936
198
    const bool need_filter_output = selected_rows != batch_rows;
1937
198
    const double batch_survival = static_cast<double>(selected_rows) / batch_rows;
1938
198
    _predicate_survival_ratio = _predicate_survival_ratio < 0
1939
198
                                        ? batch_survival
1940
198
                                        : 0.25 * batch_survival + 0.75 * _predicate_survival_ratio;
1941
198
    if (_scan_profile.selected_rows != nullptr) {
1942
120
        COUNTER_UPDATE(_scan_profile.selected_rows, selected_rows);
1943
120
    }
1944
198
    if (_scan_profile.rows_filtered_by_conjunct != nullptr) {
1945
120
        COUNTER_UPDATE(_scan_profile.rows_filtered_by_conjunct, conjunct_filtered_rows);
1946
120
    }
1947
198
    if (!_current_non_predicate_columns.empty() &&
1948
198
        _scan_profile.lazy_read_filtered_rows != nullptr) {
1949
103
        COUNTER_UPDATE(_scan_profile.lazy_read_filtered_rows, batch_rows - selected_rows);
1950
103
    }
1951
198
    if (selected_rows == 0 && _scan_profile.empty_selection_batches != nullptr) {
1952
35
        COUNTER_UPDATE(_scan_profile.empty_selection_batches, 1);
1953
163
    } else if (static_cast<int64_t>(selected_rows) == batch_rows &&
1954
163
               _scan_profile.dense_batches != nullptr) {
1955
47
        COUNTER_UPDATE(_scan_profile.dense_batches, 1);
1956
116
    } else if (_scan_profile.selected_batches != nullptr) {
1957
38
        COUNTER_UPDATE(_scan_profile.selected_batches, 1);
1958
38
    }
1959
198
    if (need_filter_output && !predicate_columns_filtered) {
1960
38
        IColumn::Filter output_filter = selection_to_filter(selection, selected_rows, batch_rows);
1961
43
        for (const auto& col : request.predicate_columns) {
1962
43
            auto position_it = request.local_positions.find(col.column_id());
1963
43
            DORIS_CHECK(position_it != request.local_positions.end());
1964
43
            const auto block_position = position_it->second.value();
1965
43
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
1966
43
                    block_position, file_block->get_by_position(block_position)
1967
43
                                            .column->filter(output_filter, selected_rows)));
1968
43
        }
1969
38
    }
1970
198
    if (selected_rows == 0) {
1971
        // Predicate readers have consumed this physical batch, but touching every lazy column here
1972
        // turns a long rejected prefix into `empty_batches * lazy_columns` native calls. Record only
1973
        // the positional lag. If [0, 32), [32, 64), and [64, 96) are empty, the first surviving
1974
        // batch performs one skip(96) per lazy column. If the row group ends instead, reset drops the
1975
        // lazy readers without flushing because no value from them can be observed.
1976
35
        DORIS_CHECK(_pending_non_predicate_skip_rows <=
1977
35
                    std::numeric_limits<int64_t>::max() - batch_rows);
1978
35
        _pending_non_predicate_skip_rows += batch_rows;
1979
35
        *rows = 0;
1980
35
        return Status::OK();
1981
35
    }
1982
163
    if (!_current_merge_range_active && selected_rows > 0 &&
1983
163
        !_current_non_predicate_columns.empty()) {
1984
        // Do not prefetch lazy output columns until at least one row survives filtering. This is
1985
        // the same decision point where the v2 reader switches from predicate-only reads to
1986
        // materializing non-predicate columns, so fully filtered batches avoid unnecessary IO.
1987
0
        RETURN_IF_ERROR(prefetch_current_row_group_columns(file_context, file_schema,
1988
0
                                                           physical_non_predicate_columns(request),
1989
0
                                                           &_current_non_predicate_prefetched));
1990
0
    }
1991
1992
163
    {
1993
163
        SCOPED_TIMER(_scan_profile.column_read_time);
1994
        // Bring lazy readers to the first row of the current physical batch before interpreting its
1995
        // selection vector. This also merges pending range gaps with fully filtered batches.
1996
163
        RETURN_IF_ERROR(flush_pending_non_predicate_skip_rows());
1997
198
        for (const auto& [fid, column_reader] : _current_non_predicate_columns) {
1998
198
            auto position_it = request.local_positions.find(format::LocalColumnId(fid));
1999
198
            DORIS_CHECK(position_it != request.local_positions.end());
2000
198
            const auto block_position = position_it->second.value();
2001
198
            auto column = file_block->get_by_position(block_position).column->assert_mutable();
2002
198
            DCHECK_EQ(file_block->get_by_position(block_position).type->get_primitive_type(),
2003
0
                      column_reader->type()->get_primitive_type())
2004
0
                    << type_to_string(file_block->get_by_position(block_position)
2005
0
                                              .type->get_primitive_type())
2006
0
                    << " " << type_to_string(column_reader->type()->get_primitive_type()) << " "
2007
0
                    << column_reader->name() << " " << fid << " " << block_position;
2008
198
            if (need_filter_output) {
2009
40
                [[maybe_unused]] auto old_size = column->size();
2010
40
                RETURN_IF_ERROR(
2011
40
                        column_reader->select(selection, selected_rows, batch_rows, column));
2012
40
                if (column->size() != old_size + selected_rows) {
2013
0
                    return Status::Corruption(
2014
0
                            "Parquet selected output column {} returned {} rows, expected {} rows",
2015
0
                            column_reader->name(), column->size(), old_size + selected_rows);
2016
0
                }
2017
158
            } else {
2018
158
                int64_t column_rows = 0;
2019
158
                RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
2020
158
                if (column_rows != batch_rows) {
2021
0
                    return Status::Corruption(
2022
0
                            "Parquet output column {} returned {} rows, expected {} rows",
2023
0
                            column_reader->name(), column_rows, batch_rows);
2024
0
                }
2025
158
            }
2026
198
            file_block->replace_by_position(block_position, std::move(column));
2027
198
        }
2028
163
    }
2029
163
    materialize_count_star_placeholders(request, selected_rows, file_block);
2030
163
    *rows = static_cast<size_t>(selected_rows);
2031
163
    return Status::OK();
2032
163
}
2033
2034
void ParquetScanScheduler::mark_condition_cache_granules(const SelectionVector& selection,
2035
                                                         uint16_t selected_rows,
2036
198
                                                         int64_t batch_first_file_row) {
2037
198
    if (!_condition_cache_ctx || _condition_cache_ctx->is_hit ||
2038
198
        !_condition_cache_ctx->filter_result) {
2039
197
        return;
2040
197
    }
2041
1
    auto& cache = *_condition_cache_ctx->filter_result;
2042
2.04k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
2043
2.04k
        const int64_t file_row = batch_first_file_row + selection.get_index(selection_idx);
2044
2.04k
        const int64_t granule = file_row / ConditionCacheContext::GRANULE_SIZE;
2045
2.04k
        const int64_t cache_idx = granule - _condition_cache_ctx->base_granule;
2046
2.04k
        if (cache_idx >= 0 && static_cast<size_t>(cache_idx) < cache.size()) {
2047
2.04k
            cache[static_cast<size_t>(cache_idx)] = true;
2048
2.04k
        }
2049
2.04k
    }
2050
1
}
2051
2052
Status ParquetScanScheduler::read_next_batch(
2053
        ParquetFileContext& file_context,
2054
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
2055
273
        const format::FileScanRequest& request, Block* file_block, size_t* rows, bool* eof) {
2056
273
    *rows = 0;
2057
273
    int64_t predicate_batch_rows = _batch_size;
2058
273
    const int64_t max_predicate_batch_rows = std::min<int64_t>(
2059
273
            std::numeric_limits<uint16_t>::max(),
2060
273
            std::max<int64_t>(DEFAULT_READ_BATCH_SIZE, _runtime_state == nullptr
2061
273
                                                               ? DEFAULT_READ_BATCH_SIZE
2062
273
                                                               : _runtime_state->batch_size()));
2063
273
    auto grow_empty_predicate_batch = [max_predicate_batch_rows](int64_t current) {
2064
35
        for (const int64_t target :
2065
120
             {int64_t {256}, int64_t {1024}, int64_t {4096}, max_predicate_batch_rows}) {
2066
120
            if (current < target) {
2067
9
                return std::min(target, max_predicate_batch_rows);
2068
9
            }
2069
120
        }
2070
26
        return max_predicate_batch_rows;
2071
35
    };
2072
425
    while (true) {
2073
425
        if (!_has_current_row_group) {
2074
280
            bool has_row_group = false;
2075
280
            RETURN_IF_ERROR(
2076
280
                    open_next_row_group(file_context, file_schema, request, &has_row_group));
2077
280
            if (!has_row_group) {
2078
99
                *eof = true;
2079
99
                return Status::OK();
2080
99
            }
2081
280
        }
2082
2083
326
        if (_current_range_idx >= _current_selected_ranges.size()) {
2084
            // Current row group finished, try next row group.
2085
117
            reset_current_row_group();
2086
117
            continue;
2087
117
        }
2088
2089
209
        const RowRange& current_range = _current_selected_ranges[_current_range_idx];
2090
209
        DORIS_CHECK(current_range.start >= 0);
2091
209
        DORIS_CHECK(current_range.length > 0);
2092
209
        DORIS_CHECK(current_range.start + current_range.length <= _current_row_group_rows);
2093
2094
209
        if (_current_row_group_rows_read < current_range.start) {
2095
            // Skip filtered rows according to row group level pruning.
2096
3
            RETURN_IF_ERROR(skip_current_row_group_rows(current_range.start -
2097
3
                                                        _current_row_group_rows_read));
2098
3
        }
2099
209
        DORIS_CHECK(_current_row_group_rows_read == current_range.start + _current_range_rows_read);
2100
209
        const int64_t remaining_rows = current_range.length - _current_range_rows_read;
2101
209
        if (remaining_rows <= 0) {
2102
            // Current range finished, try next range in the same row group.
2103
0
            ++_current_range_idx;
2104
0
            _current_range_rows_read = 0;
2105
0
            continue;
2106
0
        }
2107
2108
209
        const int64_t batch_rows = std::min<int64_t>(predicate_batch_rows, remaining_rows);
2109
209
        const int64_t physical_rows_read = batch_rows;
2110
209
        const int64_t batch_first_file_row =
2111
209
                _current_row_group_first_row + _current_row_group_rows_read;
2112
209
        RETURN_IF_ERROR(read_current_row_group_batch(file_context, file_schema, batch_rows, request,
2113
209
                                                     batch_first_file_row, file_block, rows));
2114
209
        _current_row_group_rows_read += physical_rows_read;
2115
209
        _current_range_rows_read += physical_rows_read;
2116
209
        if (_current_range_rows_read >= current_range.length) {
2117
181
            ++_current_range_idx;
2118
181
            _current_range_rows_read = 0;
2119
181
        }
2120
209
        if (*rows == 0) {
2121
            // Output-width feedback has no sample for a fully rejected batch. Grow only the
2122
            // predicate-side physical read so a 32-row probe cannot pin a long filtered prefix;
2123
            // any eventual output still stays below RuntimeState's row cap.
2124
35
            predicate_batch_rows = grow_empty_predicate_batch(predicate_batch_rows);
2125
35
            continue;
2126
35
        }
2127
174
        *eof = false;
2128
174
        return Status::OK();
2129
209
    }
2130
273
}
2131
2132
} // namespace doris::format::parquet