Coverage Report

Created: 2026-07-19 07:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/runtime/query_cache/query_cache.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 "runtime/query_cache/query_cache.h"
19
20
#include <limits>
21
22
#include "cloud/config.h"
23
#include "common/logging.h"
24
#include "common/metrics/doris_metrics.h"
25
#include "cpp/sync_point.h"
26
#include "storage/olap_common.h"
27
#include "storage/rowset/rowset.h"
28
#include "storage/rowset/rowset_reader.h"
29
#include "storage/tablet/base_tablet.h"
30
#include "storage/tablet/tablet_meta.h"
31
32
namespace doris {
33
34
10
std::vector<int>* QueryCacheHandle::get_cache_slot_orders() {
35
10
    DCHECK(_handle);
36
10
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
37
10
    return &((QueryCache::CacheValue*)(result_ptr))->slot_orders;
38
10
}
39
40
13
CacheResult* QueryCacheHandle::get_cache_result() {
41
13
    DCHECK(_handle);
42
13
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
43
13
    return &((QueryCache::CacheValue*)(result_ptr))->result;
44
13
}
45
46
66
int64_t QueryCacheHandle::get_cache_version() {
47
66
    DCHECK(_handle);
48
66
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
49
66
    return ((QueryCache::CacheValue*)(result_ptr))->version;
50
66
}
51
52
28
int64_t QueryCacheHandle::get_cache_delta_count() {
53
28
    DCHECK(_handle);
54
28
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
55
28
    return ((QueryCache::CacheValue*)(result_ptr))->delta_count;
56
28
}
57
58
8
int64_t QueryCacheHandle::get_cache_total_bytes() {
59
8
    DCHECK(_handle);
60
8
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
61
8
    return ((QueryCache::CacheValue*)(result_ptr))->total_bytes;
62
8
}
63
64
8
int64_t QueryCacheHandle::get_cache_total_rows() {
65
8
    DCHECK(_handle);
66
8
    auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value;
67
8
    return ((QueryCache::CacheValue*)(result_ptr))->total_rows;
68
8
}
69
70
void QueryCache::insert(const CacheKey& key, int64_t version, CacheResult& res,
71
                        const std::vector<int>& slot_orders, int64_t cache_size,
72
39
                        int64_t delta_count) {
73
39
    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
74
39
    CacheResult cache_result;
75
44
    for (auto& block_data : res) {
76
44
        cache_result.emplace_back(Block::create_unique())->swap(block_data->clone_empty());
77
44
        ScopedMutableBlock scoped_mutable_block(cache_result.back().get());
78
44
        auto& mutable_block = scoped_mutable_block.mutable_block();
79
44
        auto st = mutable_block.merge(*block_data);
80
44
        DORIS_CHECK(st.ok());
81
44
    }
82
39
    auto cache_value_ptr = std::make_unique<QueryCache::CacheValue>(
83
39
            version, std::move(cache_result), slot_orders, delta_count, cache_size);
84
85
39
    QueryCacheHandle(this, LRUCachePolicy::insert(key, (void*)cache_value_ptr.release(), cache_size,
86
39
                                                  cache_size, CachePriority::NORMAL));
87
39
    DorisMetrics::instance()->query_cache_write_back_total->increment(1);
88
39
}
89
90
9
bool QueryCache::lookup(const CacheKey& key, int64_t version, doris::QueryCacheHandle* handle) {
91
9
    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
92
9
    auto* lru_handle = LRUCachePolicy::lookup(key);
93
9
    if (lru_handle) {
94
9
        QueryCacheHandle tmp_handle(this, lru_handle);
95
9
        if (tmp_handle.get_cache_version() == version) {
96
5
            *handle = std::move(tmp_handle);
97
5
            return true;
98
5
        }
99
9
    }
100
4
    return false;
101
9
}
102
103
43
bool QueryCache::lookup_any_version(const CacheKey& key, QueryCacheHandle* handle) {
104
43
    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
105
43
    auto* lru_handle = LRUCachePolicy::lookup(key);
106
43
    if (lru_handle) {
107
37
        *handle = QueryCacheHandle(this, lru_handle);
108
37
        return true;
109
37
    }
110
6
    return false;
111
43
}
112
113
42
QueryCacheInstanceDecision::~QueryCacheInstanceDecision() = default;
114
115
std::unique_ptr<TabletReadSource> QueryCacheInstanceDecision::take_delta_read_source(
116
7
        int64_t tablet_id) {
117
7
    std::lock_guard<std::mutex> lock(_take_lock);
118
7
    auto it = _delta_read_sources.find(tablet_id);
119
7
    if (it == _delta_read_sources.end()) {
120
3
        return nullptr;
121
3
    }
122
4
    auto res = std::move(it->second);
123
4
    _delta_read_sources.erase(it);
124
4
    return res;
125
7
}
126
127
std::shared_ptr<QueryCacheInstanceDecision> QueryCacheRuntime::get_or_make_decision(
128
43
        const std::vector<TScanRangeParams>& scan_ranges) {
129
43
    std::string cache_key;
130
43
    int64_t version = 0;
131
43
    Status st = QueryCache::build_cache_key(scan_ranges, _param, &cache_key, &version);
132
43
    if (!st.ok()) {
133
        // No reliable cache key for this instance (e.g. FE could not align the
134
        // instance to a single partition so tablets carry different versions).
135
        // Degrade to an uncached scan instead of failing the query: both the
136
        // scan operator and the cache source operator observe the shared MISS
137
        // decision with key_valid=false, so nothing is looked up and nothing
138
        // is written back. This is an expected plan shape, so log only once
139
        // per fragment instead of twice per instance.
140
2
        std::lock_guard<std::mutex> lock(_lock);
141
2
        if (_invalid_decision == nullptr) {
142
1
            LOG(WARNING) << "query cache degrades to uncached scan, node_id=" << _param.node_id
143
1
                         << ", reason: " << st.to_string();
144
1
            _invalid_decision = std::make_shared<QueryCacheInstanceDecision>();
145
1
        }
146
2
        return _invalid_decision;
147
2
    }
148
149
41
    {
150
41
        std::lock_guard<std::mutex> lock(_lock);
151
41
        auto it = _decisions.find(cache_key);
152
41
        if (it != _decisions.end()) {
153
6
            return it->second;
154
6
        }
155
41
    }
156
157
    // Build the candidate outside the lock: a stale-entry decision walks
158
    // tablet metadata under the header lock and, on merge-on-write tables,
159
    // scans the delete bitmap, so its cost grows with the tablets per
160
    // instance and the bitmap size. This runtime is fragment-wide, so a
161
    // single lock-scoped build would serialize even unrelated instances'
162
    // keys behind one slow decision. Racing callers of the same instance
163
    // build duplicate candidates; the loser adopts the winner's decision
164
    // below and its own candidate releases with the shared_ptr (the entry
165
    // pin and any captured delta read sources go with it), which only costs
166
    // a redundant metadata pass. The metrics are settled once, by the
167
    // winner, after publication.
168
35
    auto decision = std::make_shared<QueryCacheInstanceDecision>();
169
35
    decision->key_valid = true;
170
35
    decision->cache_key = cache_key;
171
35
    decision->current_version = version;
172
35
    _make_decision(scan_ranges, decision.get());
173
35
    TEST_SYNC_POINT("QueryCacheRuntime::get_or_make_decision.before_publish");
174
175
35
    std::lock_guard<std::mutex> lock(_lock);
176
35
    auto [it, inserted] = _decisions.emplace(std::move(cache_key), decision);
177
35
    if (inserted) {
178
32
        if (decision->mode == QueryCacheInstanceDecision::Mode::INCREMENTAL) {
179
6
            DorisMetrics::instance()->query_cache_stale_hit_total->increment(1);
180
26
        } else if (decision->mode == QueryCacheInstanceDecision::Mode::MISS &&
181
26
                   !decision->incremental_fallback_reason.empty()) {
182
            // Only count real fallbacks: an empty reason means incremental
183
            // merge was not enabled for this query in the first place.
184
15
            DorisMetrics::instance()->query_cache_incremental_fallback_total->increment(1);
185
15
        }
186
32
    }
187
35
    return it->second;
188
41
}
189
190
void QueryCacheRuntime::_make_decision(const std::vector<TScanRangeParams>& scan_ranges,
191
35
                                       QueryCacheInstanceDecision* decision) {
192
35
    decision->mode = QueryCacheInstanceDecision::Mode::MISS;
193
35
    if (_binlog_scan) {
194
        // A row-binlog scan reads a different data stream under the same plan
195
        // digest: it must neither serve cached blocks nor write its output
196
        // back (that would poison the cache for normal queries).
197
1
        decision->key_valid = false;
198
1
        return;
199
1
    }
200
34
    if (_param.force_refresh_query_cache) {
201
1
        return;
202
1
    }
203
204
33
    QueryCacheHandle handle;
205
33
    if (!_cache->lookup_any_version(decision->cache_key, &handle)) {
206
5
        return;
207
5
    }
208
    // Pin the entry: as long as this decision object lives, the entry cannot be
209
    // evicted, so every operator consuming this decision sees the same blocks.
210
28
    decision->handle = std::move(handle);
211
212
28
    if (decision->handle.get_cache_version() == decision->current_version) {
213
3
        decision->mode = QueryCacheInstanceDecision::Mode::HIT;
214
3
        decision->cached_delta_count = decision->handle.get_cache_delta_count();
215
3
        return;
216
3
    }
217
218
25
    if (_try_prepare_incremental(scan_ranges, decision)) {
219
7
        decision->mode = QueryCacheInstanceDecision::Mode::INCREMENTAL;
220
7
        return;
221
7
    }
222
223
    // Stale entry not reusable: drop the pin and fall back to a full scan.
224
    // The stale-hit/fallback metrics are settled by the caller once the
225
    // winning candidate is published, so a losing racer cannot double-count.
226
18
    decision->_delta_read_sources.clear();
227
18
    decision->handle = QueryCacheHandle();
228
18
    decision->mode = QueryCacheInstanceDecision::Mode::MISS;
229
18
}
230
231
bool QueryCacheRuntime::_try_prepare_incremental(const std::vector<TScanRangeParams>& scan_ranges,
232
25
                                                 QueryCacheInstanceDecision* decision) {
233
25
    if (!(_param.__isset.allow_incremental && _param.allow_incremental)) {
234
        // Not a fallback: incremental merge is simply not enabled for this
235
        // query, so leave incremental_fallback_reason empty.
236
2
        return false;
237
2
    }
238
    // Cloud tablets capture rowsets through a different (partly asynchronous)
239
    // path; incremental merge only supports local storage for now.
240
23
    if (config::is_cloud_mode()) {
241
1
        decision->incremental_fallback_reason = "cloud mode";
242
1
        return false;
243
1
    }
244
245
22
    int64_t cached_version = decision->handle.get_cache_version();
246
22
    if (cached_version >= decision->current_version) {
247
        // The entry is newer than what this replica is asked to read (e.g. the
248
        // entry was filled from another replica with a newer visible version).
249
        // A full scan of the requested version is the only safe answer.
250
1
        decision->incremental_fallback_reason = "cached entry is newer";
251
1
        return false;
252
1
    }
253
21
    int64_t cached_delta_count = decision->handle.get_cache_delta_count();
254
21
    if (cached_delta_count >= config::query_cache_max_incremental_merge_count) {
255
        // Force a full recompute to compact the entry: every incremental merge
256
        // appends the delta blocks to the entry, so both the entry and the
257
        // upstream merge get more fragmented as deltas accumulate.
258
1
        decision->incremental_fallback_reason = "delta count reached compaction threshold";
259
1
        return false;
260
1
    }
261
262
20
    for (const auto& scan_range : scan_ranges) {
263
20
        int64_t tablet_id = scan_range.scan_range.palo_scan_range.tablet_id;
264
20
        if (!_capture_tablet_delta(tablet_id, cached_version, decision)) {
265
13
            return false;
266
13
        }
267
20
    }
268
269
    // If the cached entry alone already exceeds the entry limits, the merged
270
    // entry (which can only be larger) could never be written back. Still scan
271
    // only the delta, but tell the cache source upfront so it does not clone
272
    // blocks for a write back that would be discarded anyway. Such an entry
273
    // stays stale until compaction merges its delta away (then the capture
274
    // above fails and a full recompute takes over).
275
7
    if (decision->handle.get_cache_total_bytes() > _param.entry_max_bytes ||
276
7
        decision->handle.get_cache_total_rows() > _param.entry_max_rows) {
277
1
        decision->write_back_feasible = false;
278
1
    }
279
280
7
    decision->cached_version = cached_version;
281
7
    decision->cached_delta_count = cached_delta_count;
282
7
    return true;
283
20
}
284
285
bool QueryCacheRuntime::_capture_tablet_delta(int64_t tablet_id, int64_t cached_version,
286
20
                                              QueryCacheInstanceDecision* decision) {
287
20
    auto tablet_res = ExecEnv::get_tablet(tablet_id);
288
20
    if (!tablet_res) {
289
2
        decision->incremental_fallback_reason = "tablet not found";
290
2
        return false;
291
2
    }
292
18
    BaseTabletSPtr tablet = std::move(tablet_res.value());
293
    // Defensive re-check of what FE promised with allow_incremental:
294
    // "cached snapshot + delta rowsets == new snapshot" holds unconditionally
295
    // for append-only DUP_KEYS data, and for merge-on-write UNIQUE data as
296
    // long as the delta did not rewrite any row that predates the cached
297
    // version (verified below once the delta rowsets are known). It never
298
    // holds for merge-on-read UNIQUE data (duplicates are resolved by merging
299
    // across rowsets at read time, so a delta-only scan cannot stand alone)
300
    // nor for AGG tables (rows merge in the storage layer likewise).
301
18
    const bool merge_on_write = tablet->keys_type() == KeysType::UNIQUE_KEYS &&
302
18
                                tablet->enable_unique_key_merge_on_write();
303
18
    if (tablet->keys_type() != KeysType::DUP_KEYS && !merge_on_write) {
304
4
        decision->incremental_fallback_reason = "keys type not append-only";
305
4
        return false;
306
4
    }
307
308
    // quiet: on a version-graph miss (the delta merged away by compaction, an
309
    // expected per-query situation) this option makes the capture API neither
310
    // log nor error; it returns whatever PREFIX of the path it walked before
311
    // the miss (empty when the window start itself is gone), so emptiness AND
312
    // window coverage are both checked below as the failure signals.
313
14
    auto source_res = tablet->capture_read_source({cached_version + 1, decision->current_version},
314
14
                                                  {.quiet = true});
315
14
    if (!source_res) {
316
        // Non-pathfinding failures (e.g. a rowset object missing for a found
317
        // path) still surface as errors even under quiet.
318
1
        decision->incremental_fallback_reason = "delta versions not capturable";
319
1
        return false;
320
1
    }
321
13
    auto read_source = std::make_unique<TabletReadSource>(std::move(source_res.value()));
322
13
    if (read_source->rs_splits.empty()) {
323
        // A non-empty version window always lies on a consistent path with at
324
        // least one rowset; an empty capture therefore means the delta
325
        // versions are gone, never "no new data". Treating it as INCREMENTAL
326
        // would silently drop the delta rows from the result.
327
1
        decision->incremental_fallback_reason = "delta versions not capturable";
328
1
        return false;
329
1
    }
330
    // A quiet capture broken mid-window (e.g. a replica whose local view ends
331
    // short of current_version) yields a partial prefix, and treating it as
332
    // INCREMENTAL would silently drop the tail rows and poison the entry on
333
    // write-back. The path is contiguous by construction, so checking both
334
    // endpoints pins the whole (cached_version, current_version] window.
335
12
    if (read_source->rs_splits.front().rs_reader->rowset()->start_version() != cached_version + 1 ||
336
12
        read_source->rs_splits.back().rs_reader->rowset()->end_version() !=
337
12
                decision->current_version) {
338
1
        decision->incremental_fallback_reason = "delta versions not capturable";
339
1
        return false;
340
1
    }
341
11
    read_source->fill_delete_predicates();
342
11
    if (!read_source->delete_predicates.empty()) {
343
        // A delete in the delta logically removes rows that are already folded
344
        // into the cached partial result; that cannot be undone by merging, so
345
        // fall back to a full recompute.
346
1
        decision->incremental_fallback_reason = "delta contains delete predicates";
347
1
        return false;
348
1
    }
349
    // Pin the cached side for as long as the classification below reads the
350
    // delete bitmap. The markers it looks for sit on the rowsets the cached
351
    // snapshot was built from, and the capture above pins only the delta
352
    // side, so nothing else keeps that evidence alive: once a compaction
353
    // spanning both sides retires such a rowset, an overwritten row has no
354
    // counterpart in the output and its marker is therefore not relocated
355
    // (BaseTablet::calc_compaction_output_rowset_delete_bitmap skips a source
356
    // row whose id does not convert), leaving the marker only on the retired
357
    // input, which the stale sweep then hands to the unused-rowset GC.
358
    // Classification would see no rewrite and merge the cached rows with
359
    // their replacements: a wrong entry, stored at the current version.
360
    // Holding the rowsets stops that, in start_delete_unused_rowset(): it
361
    // collects a retired rowset only once nothing else references it, and
362
    // only what it collects gets remove_rowset_delete_bitmap(), which drops
363
    // every version of that rowset's bitmap. The other remover, the key
364
    // ranges that the stale sweep queues onto the pre-window rowsets, is
365
    // held off by the delta capture instead: that queue is released only
366
    // once the swept path's own rowsets are unreferenced.
367
10
    std::vector<RowsetSharedPtr> cached_side_pin;
368
10
    if (merge_on_write) {
369
6
        {
370
6
            std::shared_lock rdlock(tablet->get_header_lock());
371
6
            auto pin_res = tablet->capture_consistent_rowsets_unlocked({0, cached_version},
372
6
                                                                       {.quiet = true});
373
6
            if (pin_res) {
374
6
                cached_side_pin = std::move(pin_res.value().rowsets);
375
6
            }
376
6
        }
377
        // quiet yields the prefix it walked, so cover the whole cached range
378
        // before trusting the classification: an unpinned tail is evidence we
379
        // cannot vouch for.
380
6
        if (cached_side_pin.empty() || cached_side_pin.front()->start_version() != 0 ||
381
6
            cached_side_pin.back()->end_version() != cached_version) {
382
1
            decision->incremental_fallback_reason = "cached versions not pinnable";
383
1
            return false;
384
1
        }
385
        // The pin is only worth anything while the classification runs, so
386
        // let a test observe it exactly there rather than infer it.
387
6
        TEST_SYNC_POINT_CALLBACK("QueryCacheRuntime::_capture_tablet_delta.cached_side_pinned",
388
5
                                 &cached_side_pin);
389
5
    }
390
9
    if (merge_on_write &&
391
9
        _delta_rewrites_history(*tablet, *read_source, cached_version, decision->current_version)) {
392
        // A load in the delta window marked rows of the cached snapshot as
393
        // deleted through the delete bitmap (an upsert, a partial update or a
394
        // delete sign hit a pre-existing key). Those rows are already folded
395
        // into the cached partial result and cannot be subtracted, so only a
396
        // full recompute is safe. The full run re-bases the entry at the new
397
        // version, so an occasional backfill costs exactly one full scan and
398
        // the next pure-append hour is incremental again.
399
2
        decision->incremental_fallback_reason = "delta rewrites history rows";
400
2
        return false;
401
2
    }
402
7
    decision->_delta_read_sources[tablet_id] = std::move(read_source);
403
7
    return true;
404
9
}
405
406
bool QueryCacheRuntime::_delta_rewrites_history(BaseTablet& tablet,
407
                                                const TabletReadSource& delta_source,
408
5
                                                int64_t cached_version, int64_t current_version) {
409
5
    RowsetIdUnorderedSet delta_rowsets;
410
6
    for (const auto& split : delta_source.rs_splits) {
411
6
        delta_rowsets.insert(split.rs_reader->rowset()->rowset_id());
412
6
    }
413
    // The tablet delete bitmap is shared and it does change at versions that
414
    // are already visible, so what keeps this scan safe is not immutability
415
    // but the fallback it feeds. Loads add their markers before the version
416
    // becomes visible. Compaction relocates existing markers onto its output
417
    // rowset keeping their original version (BaseTablet::calc_compaction_
418
    // output_rowset_delete_bitmap); markers that matter here sit on a rowset
419
    // at or below the cached version, and an output that swallowed such a
420
    // rowset starts no later than it does, so the relocated marker lands
421
    // outside the delta and is read as a rewrite: conservative, never a miss.
422
    // (An output whose inputs all sit inside the window does belong to the
423
    // delta, and markers on it are delta-internal, which is what we want.) The one
424
    // rewrite that moves markers out of the window, the aggregation that
425
    // re-stamps a stale range onto its end version (BaseTablet::agg_delete_
426
    // bitmap_for_stale_rowsets, run by delete_expired_stale_rowset), only
427
    // fires once the merged versions are swept, and the capture this scan
428
    // pairs with anchors on the window endpoints, so a window whose path is
429
    // gone is rejected before we get here. Capture and scan are not atomic,
430
    // so a sweep landing exactly in between (with the aggregated keys also
431
    // already recycled) can still hide a rewrite: a known narrow race the
432
    // fallback does not cover, shared with the baseline exact-hit path.
433
    // Pending entries use DeleteBitmap::TEMP_VERSION_COMMON (= 0) and stay
434
    // below the window as well.
435
    //
436
    // The map is ordered by (rowset, segment, version), and most entries of
437
    // a group lie OUTSIDE the delta window (dedup history below it, pending
438
    // entries at 0, concurrent newer loads above it), so on a long-lived
439
    // merge-on-write tablet the map is much larger than the window. Hop from
440
    // group to group with lower_bound/upper_bound instead of visiting every
441
    // entry: the shared-lock hold, which excludes bitmap writers such as
442
    // load publish, is then bounded by the in-window entries plus one
443
    // logarithmic hop per group instead of the full map size.
444
    // Compare versions in the uint64 key domain (both window bounds are
445
    // non-negative), so a hypothetical out-of-int64-range stored version can
446
    // only take the above-window hop, whose target is strictly past the
447
    // whole group: forward progress holds for arbitrary map contents, like
448
    // the plain walk this replaces.
449
5
    const auto window_begin = static_cast<uint64_t>(cached_version) + 1;
450
5
    const auto window_end = static_cast<uint64_t>(current_version);
451
5
    const DeleteBitmap& delete_bitmap = tablet.tablet_meta()->delete_bitmap();
452
5
    std::shared_lock rdlock(delete_bitmap.lock);
453
5
    const auto& bitmap_map = delete_bitmap.delete_bitmap;
454
5
    auto it = bitmap_map.begin();
455
11
    while (it != bitmap_map.end()) {
456
8
        const auto& [rowset_id, segment_id, version_key] = it->first;
457
8
        if (version_key < window_begin) {
458
1
            it = bitmap_map.lower_bound({rowset_id, segment_id, window_begin});
459
1
            continue;
460
1
        }
461
7
        if (version_key > window_end) {
462
3
            it = bitmap_map.upper_bound(
463
3
                    {rowset_id, segment_id, std::numeric_limits<uint64_t>::max()});
464
3
            continue;
465
3
        }
466
4
        if (!it->second.isEmpty() && !delta_rowsets.contains(rowset_id)) {
467
2
            return true;
468
2
        }
469
2
        ++it;
470
2
    }
471
3
    return false;
472
5
}
473
474
} // namespace doris