Coverage Report

Created: 2026-07-15 04:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exec/scan/parallel_scanner_builder.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
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "exec/scan/parallel_scanner_builder.h"
19
20
#include <cstddef>
21
22
#include "cloud/cloud_storage_engine.h"
23
#include "cloud/cloud_tablet_hotspot.h"
24
#include "cloud/config.h"
25
#include "common/status.h"
26
#include "exec/operator/olap_scan_operator.h"
27
#include "exec/scan/olap_scanner.h"
28
#include "io/io_common.h"
29
#include "runtime/query_context.h"
30
#include "storage/rowset/beta_rowset.h"
31
#include "storage/segment/segment_loader.h"
32
#include "storage/tablet/base_tablet.h"
33
34
namespace doris {
35
36
namespace {
37
38
io::FileCacheStatistics take_initial_file_cache_stats(
39
680k
        std::unordered_map<int64_t, io::FileCacheStatistics>* preload_stats, int64_t tablet_id) {
40
680k
    auto it = preload_stats->find(tablet_id);
41
680k
    if (it == preload_stats->end()) {
42
55.3k
        return {};
43
55.3k
    }
44
625k
    auto stats = std::move(it->second);
45
625k
    preload_stats->erase(it);
46
625k
    return stats;
47
680k
}
48
49
3.50M
io::IOContext create_preload_io_context(RuntimeState* state, OlapReaderStatistics* preload_stats) {
50
3.50M
    io::IOContext io_ctx;
51
3.50M
    io_ctx.reader_type = ReaderType::READER_QUERY;
52
18.4E
    io_ctx.file_cache_stats = preload_stats ? &preload_stats->file_cache_stats : nullptr;
53
3.50M
    if (state == nullptr) {
54
0
        return io_ctx;
55
0
    }
56
3.50M
    io_ctx.query_id = &state->query_id();
57
3.50M
    io_ctx.read_file_cache = state->query_options().enable_file_cache;
58
3.50M
    io_ctx.is_disposable = state->query_options().disable_file_cache;
59
3.50M
    if (auto* query_ctx = state->get_query_ctx(); query_ctx != nullptr) {
60
3.50M
        io_ctx.remote_scan_cache_write_limiter = query_ctx->remote_scan_cache_write_limiter();
61
3.50M
    }
62
3.50M
    return io_ctx;
63
3.50M
}
64
65
} // namespace
66
67
177k
Status ParallelScannerBuilder::build_scanners(std::list<ScannerSPtr>& scanners) {
68
177k
    RETURN_IF_ERROR(_load());
69
177k
    if (_scan_parallelism_by_per_segment) {
70
28
        return _build_scanners_by_per_segment(scanners);
71
179k
    } else if (_is_dup_mow_key) {
72
        // Default strategy for DUP/MOW tables: split by rowids within segments
73
179k
        return _build_scanners_by_rowid(scanners);
74
18.4E
    } else {
75
        // TODO: support to split by key range
76
18.4E
        return Status::NotSupported("split by key range not supported yet.");
77
18.4E
    }
78
177k
}
79
80
179k
Status ParallelScannerBuilder::_build_scanners_by_rowid(std::list<ScannerSPtr>& scanners) {
81
179k
    DCHECK_GE(_rows_per_scanner, _min_rows_per_scanner);
82
83
865k
    for (auto&& [tablet, version] : _tablets) {
84
865k
        DCHECK(_all_read_sources.contains(tablet->tablet_id()));
85
865k
        auto& entire_read_source = _all_read_sources[tablet->tablet_id()];
86
87
865k
        if (config::is_cloud_mode()) {
88
            // FIXME(plat1ko): Avoid pointer cast
89
495k
            ExecEnv::GetInstance()->storage_engine().to_cloud().tablet_hotspot().count(*tablet);
90
495k
        }
91
92
        // `rs_splits` in `entire read source` will be devided into several partitial read sources
93
        // to build several parallel scanners, based on segment rows number. All the partitial read sources
94
        // share the same delete predicates from their corresponding entire read source.
95
865k
        TabletReadSource partitial_read_source;
96
865k
        int64_t rows_collected = 0;
97
3.51M
        for (auto& rs_split : entire_read_source.rs_splits) {
98
3.51M
            auto reader = rs_split.rs_reader;
99
3.51M
            auto rowset = reader->rowset();
100
3.51M
            const auto rowset_id = rowset->rowset_id();
101
102
3.51M
            const auto& segments_rows = _all_segments_rows[rowset_id];
103
104
3.51M
            if (rowset->num_rows() == 0) {
105
2.27M
                continue;
106
2.27M
            }
107
108
1.24M
            int64_t segment_start = 0;
109
1.24M
            auto split = RowSetSplits(reader->clone());
110
111
2.48M
            for (size_t i = 0; i != segments_rows.size(); ++i) {
112
1.24M
                const size_t rows_of_segment = segments_rows[i];
113
1.24M
                RowRanges row_ranges;
114
1.24M
                int64_t offset_in_segment = 0;
115
116
                // try to split large segments into RowRanges
117
2.53M
                while (offset_in_segment < rows_of_segment) {
118
1.29M
                    const int64_t remaining_rows = rows_of_segment - offset_in_segment;
119
1.29M
                    auto rows_need = _rows_per_scanner - rows_collected;
120
121
                    // 0.9: try to avoid splitting the segments into excessively small parts.
122
1.29M
                    if (rows_need >= remaining_rows * 9 / 10) {
123
1.24M
                        rows_need = remaining_rows;
124
1.24M
                    }
125
1.29M
                    DCHECK_LE(rows_need, remaining_rows);
126
127
                    // RowRange stands for range: [From, To), From is inclusive, To is exclusive.
128
1.29M
                    row_ranges.add({offset_in_segment,
129
1.29M
                                    offset_in_segment + static_cast<int64_t>(rows_need)});
130
1.29M
                    rows_collected += rows_need;
131
1.29M
                    offset_in_segment += rows_need;
132
133
                    // If collected enough rows, build a new scanner
134
1.29M
                    if (rows_collected >= _rows_per_scanner) {
135
55.8k
                        split.segment_offsets.first = segment_start,
136
55.8k
                        split.segment_offsets.second = i + 1;
137
55.8k
                        split.segment_row_ranges.emplace_back(std::move(row_ranges));
138
139
55.8k
                        DCHECK_EQ(split.segment_offsets.second - split.segment_offsets.first,
140
55.8k
                                  split.segment_row_ranges.size());
141
142
55.8k
                        partitial_read_source.rs_splits.emplace_back(std::move(split));
143
144
55.8k
                        scanners.emplace_back(_build_scanner(
145
55.8k
                                tablet, version, _key_ranges,
146
55.8k
                                {.rs_splits = std::move(partitial_read_source.rs_splits),
147
55.8k
                                 .delete_predicates = entire_read_source.delete_predicates,
148
55.8k
                                 .delete_bitmap = entire_read_source.delete_bitmap},
149
55.8k
                                take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
150
55.8k
                                                              tablet->tablet_id())));
151
152
55.8k
                        partitial_read_source = {};
153
55.8k
                        split = RowSetSplits(reader->clone());
154
55.8k
                        row_ranges = RowRanges();
155
156
55.8k
                        segment_start = offset_in_segment < rows_of_segment ? i : i + 1;
157
55.8k
                        rows_collected = 0;
158
55.8k
                    }
159
1.29M
                }
160
161
                // The non-empty `row_ranges` means there are some rows left in this segment not added into `split`.
162
1.24M
                if (!row_ranges.is_empty()) {
163
1.23M
                    DCHECK_GT(rows_collected, 0);
164
1.23M
                    DCHECK_EQ(row_ranges.to(), rows_of_segment);
165
1.23M
                    split.segment_row_ranges.emplace_back(std::move(row_ranges));
166
1.23M
                }
167
1.24M
            }
168
169
1.24M
            DCHECK_LE(rows_collected, _rows_per_scanner);
170
1.24M
            if (rows_collected > 0) {
171
1.23M
                split.segment_offsets.first = segment_start;
172
1.23M
                split.segment_offsets.second = segments_rows.size();
173
1.23M
                DCHECK_GT(split.segment_offsets.second, split.segment_offsets.first);
174
1.23M
                DCHECK_EQ(split.segment_row_ranges.size(),
175
1.23M
                          split.segment_offsets.second - split.segment_offsets.first);
176
1.23M
                partitial_read_source.rs_splits.emplace_back(std::move(split));
177
1.23M
            }
178
1.24M
        } // end `for (auto& rowset : rowsets)`
179
180
865k
        DCHECK_LE(rows_collected, _rows_per_scanner);
181
865k
        if (rows_collected > 0) {
182
626k
            DCHECK_GT(partitial_read_source.rs_splits.size(), 0);
183
626k
#ifndef NDEBUG
184
1.17M
            for (auto& split : partitial_read_source.rs_splits) {
185
1.17M
                DCHECK(split.rs_reader != nullptr);
186
1.17M
                DCHECK_LT(split.segment_offsets.first, split.segment_offsets.second);
187
1.17M
                DCHECK_EQ(split.segment_row_ranges.size(),
188
1.17M
                          split.segment_offsets.second - split.segment_offsets.first);
189
1.17M
            }
190
626k
#endif
191
626k
            scanners.emplace_back(
192
626k
                    _build_scanner(tablet, version, _key_ranges,
193
626k
                                   {.rs_splits = std::move(partitial_read_source.rs_splits),
194
626k
                                    .delete_predicates = entire_read_source.delete_predicates,
195
626k
                                    .delete_bitmap = entire_read_source.delete_bitmap},
196
626k
                                   take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
197
626k
                                                                 tablet->tablet_id())));
198
626k
        }
199
865k
    }
200
201
179k
    return Status::OK();
202
179k
}
203
204
// Build scanners so that each segment is exclusively scanned by a single scanner.
205
// This guarantees the number of scanners equals the number of segments across all rowsets
206
// for the involved tablets. It preserves delete predicates and key ranges, and clones
207
// RowsetReader per scanner to avoid sharing between scanners.
208
28
Status ParallelScannerBuilder::_build_scanners_by_per_segment(std::list<ScannerSPtr>& scanners) {
209
28
    DCHECK_GE(_rows_per_scanner, _min_rows_per_scanner);
210
211
130
    for (auto&& [tablet, version] : _tablets) {
212
130
        DCHECK(_all_read_sources.contains(tablet->tablet_id()));
213
130
        auto& entire_read_source = _all_read_sources[tablet->tablet_id()];
214
215
130
        if (config::is_cloud_mode()) {
216
            // FIXME(plat1ko): Avoid pointer cast
217
130
            ExecEnv::GetInstance()->storage_engine().to_cloud().tablet_hotspot().count(*tablet);
218
130
        }
219
220
        // For each RowSet split in the read source, split by segment id and build
221
        // one scanner per segment. Keep delete predicates shared.
222
273
        for (auto& rs_split : entire_read_source.rs_splits) {
223
273
            auto reader = rs_split.rs_reader;
224
273
            auto rowset = reader->rowset();
225
273
            const auto rowset_id = rowset->rowset_id();
226
273
            const auto& segments_rows = _all_segments_rows[rowset_id];
227
273
            if (segments_rows.empty() || rowset->num_rows() == 0) {
228
218
                continue;
229
218
            }
230
231
            // Build scanners for [i, i+1) segment range, without row-range slicing.
232
110
            for (int64_t i = 0; i < rowset->num_segments(); ++i) {
233
55
                RowSetSplits split(reader->clone());
234
55
                split.segment_offsets.first = i;
235
55
                split.segment_offsets.second = i + 1;
236
                // No row-ranges slicing; scan whole segment i.
237
55
                DCHECK_GE(split.segment_offsets.second, split.segment_offsets.first + 1);
238
239
55
                TabletReadSource partitial_read_source;
240
55
                partitial_read_source.rs_splits.emplace_back(std::move(split));
241
242
55
                scanners.emplace_back(_build_scanner(
243
55
                        tablet, version, _key_ranges,
244
55
                        {.rs_splits = std::move(partitial_read_source.rs_splits),
245
55
                         .delete_predicates = entire_read_source.delete_predicates,
246
55
                         .delete_bitmap = entire_read_source.delete_bitmap},
247
55
                        take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
248
55
                                                      tablet->tablet_id())));
249
55
            }
250
55
        }
251
130
    }
252
253
28
    return Status::OK();
254
28
}
255
256
/**
257
 * Load rowsets of each tablet with specified version, segments of each rowset.
258
 */
259
177k
Status ParallelScannerBuilder::_load() {
260
177k
    _total_rows = 0;
261
177k
    size_t idx = 0;
262
177k
    bool enable_segment_cache = _state->query_options().__isset.enable_segment_cache
263
177k
                                        ? _state->query_options().enable_segment_cache
264
177k
                                        : true;
265
862k
    for (auto&& [tablet, version] : _tablets) {
266
862k
        const auto tablet_id = tablet->tablet_id();
267
862k
        _all_read_sources[tablet_id] = _read_sources[idx];
268
862k
        const auto& read_source = _all_read_sources[tablet_id];
269
3.50M
        for (auto& rs_split : read_source.rs_splits) {
270
3.50M
            auto rowset = rs_split.rs_reader->rowset();
271
3.50M
            RETURN_IF_ERROR(rowset->load());
272
3.50M
            const auto rowset_id = rowset->rowset_id();
273
274
3.50M
            auto beta_rowset = std::dynamic_pointer_cast<BetaRowset>(rowset);
275
3.50M
            std::vector<uint32_t> segment_rows;
276
3.50M
            OlapReaderStatistics preload_stats;
277
3.50M
            auto preload_io_ctx = create_preload_io_context(_state, &preload_stats);
278
3.50M
            RETURN_IF_ERROR(beta_rowset->get_segment_num_rows(&segment_rows, enable_segment_cache,
279
3.50M
                                                              &preload_stats, &preload_io_ctx));
280
3.50M
            _tablet_preload_file_cache_stats[tablet_id].merge_from(preload_stats.file_cache_stats);
281
3.50M
            auto segment_count = rowset->num_segments();
282
4.74M
            for (int64_t i = 0; i != segment_count; i++) {
283
1.23M
                _all_segments_rows[rowset_id].emplace_back(segment_rows[i]);
284
1.23M
            }
285
3.50M
            _total_rows += rowset->num_rows();
286
3.50M
        }
287
862k
        idx++;
288
862k
    }
289
290
177k
    _rows_per_scanner = _total_rows / _max_scanners_count;
291
177k
    _rows_per_scanner = std::max<size_t>(_rows_per_scanner, _min_rows_per_scanner);
292
293
177k
    return Status::OK();
294
177k
}
295
296
std::shared_ptr<OlapScanner> ParallelScannerBuilder::_build_scanner(
297
        BaseTabletSPtr tablet, int64_t version, const std::vector<OlapScanRange*>& key_ranges,
298
679k
        TabletReadSource&& read_source, io::FileCacheStatistics&& initial_file_cache_stats) {
299
679k
    OlapScanner::Params params {
300
679k
            .state = _state,
301
679k
            .profile = _scanner_profile.get(),
302
679k
            .key_ranges = key_ranges,
303
679k
            .tablet = std::move(tablet),
304
679k
            .version = version,
305
679k
            .read_source = std::move(read_source),
306
679k
            .initial_file_cache_stats = std::move(initial_file_cache_stats),
307
679k
            .limit = _limit,
308
679k
            .aggregation = _is_preaggregation,
309
679k
            .read_row_binlog = false,
310
679k
            .binlog_scan_type = TBinlogScanType::NONE,
311
679k
            .start_tso = std::nullopt,
312
679k
            .end_tso = std::nullopt,
313
679k
    };
314
679k
    return OlapScanner::create_shared(_parent, std::move(params));
315
679k
}
316
317
} // namespace doris