Skip to content

[feature](query-cache) Support incremental merge for stale query cache entries#65482

Merged
HappenLee merged 10 commits into
apache:masterfrom
asdf2014:query-cache-incremental-merge
Jul 18, 2026
Merged

[feature](query-cache) Support incremental merge for stale query cache entries#65482
HappenLee merged 10 commits into
apache:masterfrom
asdf2014:query-cache-incremental-merge

Conversation

@asdf2014

Copy link
Copy Markdown
Member

What problem does this PR solve?

Related PR: N/A

Problem Summary:

For hourly batch-load workloads, every load bumps the tablet version of the
hot partition, so its query cache entries never hit again: each "last N days"
aggregation recomputes the whole hot partition every hour, and BE CPU spikes
during load windows.

This PR adds incremental merge for the query cache (experimental, default
off). When a cache entry's version falls behind, BE reuses it instead of
discarding it: it scans only the delta rowsets in
(cached_version, current_version], emits their partial aggregation state
side by side with the cached partial blocks so the existing upstream merge
aggregation combines both, and writes the merged entry back under the new
version. The execution plan is unchanged.

Full recompute vs incremental merge

Measured impact (80M-row duplicate-key table, 200k-row hourly appends,
group by a non-distribution column): a stale query drops from ~307 ms
(full recompute) to ~19 ms with incremental merge, about 16x and on
par with an exact hit (~17 ms); the cost no longer grows with the base data
size (8M rows: 48 ms full vs 19 ms incremental; 80M rows: 307 ms vs 19 ms).
Full numbers in the Verification section below.

Design highlights:

  1. Correctness algebra: S(v2) = S(v1) ⊎ Δ for append-only data plus the
    homomorphism partial(A ⊎ B) ≡ merge(partial(A), partial(B)). Every
    precondition guards one of the two properties; any violation falls back
    to a full recompute silently, so results are always correct.
  2. FE authorizes via a new optional thrift field
    (TQueryCacheParam.allow_incremental) only for non-finalize aggregations
    directly above the olap scan on an append-only index: DUP_KEYS, or
    merge-on-write UNIQUE_KEYS. Merge-on-read UNIQUE and AGG tables always
    fall back.
  3. BE re-validates per tablet at decision time (fallback reasons are visible
    in the profile): cloud mode, version order, the compaction threshold
    (query_cache_max_incremental_merge_count, default 8, 0 disables), keys
    type, delta capturability on the version graph, delete predicates in the
    delta, and for merge-on-write tables a delete-bitmap window check that
    rejects loads rewriting pre-existing keys. A rare backfill therefore costs
    exactly one full recompute, which re-bases the entry, and the next
    pure-append load is incremental again.
  4. A fragment-level QueryCacheRuntime makes one idempotent decision per
    instance (HIT / INCREMENTAL / MISS), pins the entry and pre-captures the
    delta read source. This also fixes three pre-existing defects: the
    scan/cache-source double-lookup race (could write back an empty poisoned
    entry), row-binlog scans polluting the cache, and build_cache_key
    failures aborting the query instead of degrading to uncached execution.
  5. Entropy control: every merge appends the delta blocks to the entry, so
    after query_cache_max_incremental_merge_count merges the next query
    recomputes in full and compacts the entry; oversized entries keep the
    delta-scan benefit but skip the write back.

Verification:

  • FE UT: QueryCacheNormalizerTest 13/13 (8 assertions on the incremental
    authorization matrix: switch, plan shapes, DUP / MoW / MoR / AGG / nested
    aggregation).

  • BE UT: 34/34 across the decision layer, the incremental scenarios (MoW
    pure-append / history-rewrite / irrelevant bitmap entries, version gap,
    capture error via debug point, delete predicates) and the operator layer.
    llvm-cov: zero uncovered changed lines; query_cache.cpp at 100% line
    coverage.

  • Regression: query_cache_incremental passes on a real 1FE+1BE cluster;
    every step is checked against a cache-off baseline. BE metrics confirm the
    incremental path actually fires (stale_hit_total +40 across the suite,
    fallbacks exactly at the three designed spots).

  • Benchmark (80M-row DUP table, 200k-row hourly appends, group by a
    non-distribution column, 5 rounds each):

    scenario latency (median)
    no cache, full scan ~446 ms
    exact hit ~17 ms
    stale + incremental merge (this PR) ~19 ms
    stale + full recompute (switch off) ~307 ms

    A stale query becomes as cheap as an exact hit, and its cost no longer
    grows with the base data size (8M rows: 48ms full vs 19ms incremental;
    80M rows: 307ms vs 19ms).

Release note

Add experimental incremental merge for the query cache: a stale entry can be
reused by scanning only the delta rowsets and merging them with the cached
partial aggregation state. Controlled by the session variable
enable_query_cache_incremental (default off) and the BE config
query_cache_max_incremental_merge_count (default 8).

Check List (For Author)

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

…e entries

Hourly batch loads keep bumping the version of the hot partition, so its
query cache entries never hit again and every "last N days" aggregation
recomputes the whole partition each hour. This change lets BE reuse a
stale entry instead of discarding it: scan only the delta rowsets in
(cached_version, current_version], emit their partial aggregation state
side by side with the cached partial blocks (the upstream merge
aggregation combines both), and write the merged entry back under the
new version.

Correctness rests on two properties: append-only snapshots decompose as
S(v2) = S(v1) + delta, and partial aggregation states merge
homomorphically. Every precondition guards one of them, and any
violation falls back to a full recompute silently:

- FE only sets TQueryCacheParam.allow_incremental (new optional field 8)
  when the experimental session switch enable_query_cache_incremental is
  on, the cache point is a non-finalize aggregation directly above the
  olap scan node, and the selected index is append-only: DUP_KEYS, or
  merge-on-write UNIQUE_KEYS.
- BE re-validates per tablet at decision time: local storage mode,
  version order, the compaction threshold
  (query_cache_max_incremental_merge_count, BE config, default 8), keys
  type, delta capturability, no delete predicates in the delta, and for
  merge-on-write tables a delete-bitmap window check that rejects any
  load that rewrote pre-existing keys, so an occasional backfill costs
  exactly one full recompute and re-bases the entry.

A new fragment-level QueryCacheRuntime makes one idempotent decision
(HIT / INCREMENTAL / MISS) per instance, pins the entry and pre-captures
the delta read source. Centralizing the decision also fixes three
pre-existing defects: the scan/cache-source double-lookup race that
could write back an empty poisoned entry, row-binlog scans polluting
the cache, and build_cache_key failures aborting the whole query
instead of degrading to uncached execution.

Observability: profile fields HitCacheStale, IncrementalDeltaVersions
and IncrementalFallbackReason, plus three BE metrics
(query_cache_stale_hit_total, query_cache_incremental_fallback_total,
query_cache_write_back_total).

Benchmark (80M-row duplicate-key table, 200k-row hourly appends,
group by a non-distribution column): a stale query drops from ~307ms
(full recompute) to ~19ms with incremental merge, on par with an exact
hit, and the cost no longer grows with the base data size.
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman morningman self-assigned this Jul 12, 2026
924060929
924060929 previously approved these changes Jul 13, 2026

@924060929 924060929 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for FE part

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@morrySnow

Copy link
Copy Markdown
Contributor

run buildall

@morrySnow

Copy link
Copy Markdown
Contributor

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found three issues, all in the test/coverage layer for the query-cache incremental merge change. The BE/FE/storage implementation paths I reviewed did not reveal a substantiated data-correctness issue, but the current tests do not yet prove the actual stale incremental SQL path and one BE fixture masks setup failures.

Critical checkpoint conclusions:

  • Goal/test proof: incomplete; the SQL regression does not assert that Mode::INCREMENTAL is reached.
  • Scope/focus: the implementation is focused on query-cache incremental merge.
  • Concurrency/lifecycle: shared cache decision, pinned cache handles, and pipeline local-state setup were reviewed; no reportable issue found.
  • FE-BE/thrift compatibility: the optional field is appended and old/new mixed behavior falls back safely.
  • Storage/version correctness: delta rowset capture, MOW delete bitmap checks, compaction fallback, and cloud fallback were reviewed; no reportable issue found.
  • Testing standards: issues found in the regression and BE fixture, reported inline.

Validation: static review only; I did not run builds or tests in this review-only runner.

Comment thread regression-test/suites/query_p0/cache/query_cache_incremental.groovy Outdated
Comment thread be/test/exec/pipeline/query_cache_test.cpp Outdated
@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 90.48% (19/21) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29834 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit b4294bb5dba96c044c13e542a65bd4e39722204e, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17621	4150	4246	4150
q2	2035	328	209	209
q3	10770	1469	851	851
q4	4809	475	341	341
q5	8337	871	569	569
q6	315	170	141	141
q7	822	836	631	631
q8	10575	1647	1703	1647
q9	5822	4415	4382	4382
q10	6818	1783	1542	1542
q11	504	354	312	312
q12	720	553	441	441
q13	18122	3861	2789	2789
q14	275	265	242	242
q15	q16	790	788	717	717
q17	996	972	991	972
q18	6664	5779	5604	5604
q19	1170	1263	1012	1012
q20	732	636	526	526
q21	5600	2678	2455	2455
q22	437	364	301	301
Total cold run time: 103934 ms
Total hot run time: 29834 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4414	4335	4369	4335
q2	295	318	221	221
q3	4583	5019	4406	4406
q4	2168	2137	1447	1447
q5	4526	4383	4675	4383
q6	251	195	139	139
q7	2101	1826	1629	1629
q8	2488	2235	2184	2184
q9	8027	7909	7961	7909
q10	4749	4967	4349	4349
q11	630	433	403	403
q12	755	756	569	569
q13	3253	3640	2972	2972
q14	313	318	282	282
q15	q16	739	729	644	644
q17	1379	1366	1356	1356
q18	7985	7349	7021	7021
q19	1110	1072	1091	1072
q20	2220	2229	1951	1951
q21	5343	4678	4585	4585
q22	519	469	423	423
Total cold run time: 57848 ms
Total hot run time: 52280 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 180384 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit b4294bb5dba96c044c13e542a65bd4e39722204e, data reload: false

query5	4336	625	499	499
query6	492	218	204	204
query7	4921	596	347	347
query8	332	194	175	175
query9	8757	4093	4061	4061
query10	480	345	284	284
query11	5984	2371	2165	2165
query12	163	102	104	102
query13	1256	621	443	443
query14	6310	5301	4984	4984
query14_1	4305	4370	4298	4298
query15	221	218	182	182
query16	1068	487	475	475
query17	1142	711	600	600
query18	2710	480	359	359
query19	212	197	155	155
query20	112	112	107	107
query21	228	152	135	135
query22	13614	13623	13411	13411
query23	17426	16577	16137	16137
query23_1	16358	16256	16261	16256
query24	7469	1783	1322	1322
query24_1	1315	1311	1272	1272
query25	562	491	410	410
query26	1356	347	219	219
query27	2532	603	385	385
query28	4435	2006	1964	1964
query29	1118	648	513	513
query30	342	261	224	224
query31	1113	1094	972	972
query32	116	65	65	65
query33	528	332	254	254
query34	1145	1145	657	657
query35	789	799	685	685
query36	1443	1401	1279	1279
query37	159	113	94	94
query38	1881	1712	1672	1672
query39	944	926	896	896
query39_1	878	890	875	875
query40	254	164	184	164
query41	64	63	67	63
query42	96	91	90	90
query43	332	331	273	273
query44	1381	797	769	769
query45	195	198	179	179
query46	1065	1225	783	783
query47	2433	2325	2206	2206
query48	355	429	290	290
query49	576	422	319	319
query50	1066	427	334	334
query51	10850	10805	10726	10726
query52	87	93	74	74
query53	250	280	200	200
query54	280	240	234	234
query55	76	71	66	66
query56	301	311	301	301
query57	1446	1413	1322	1322
query58	280	262	271	262
query59	1588	1676	1463	1463
query60	304	267	285	267
query61	159	150	156	150
query62	691	663	574	574
query63	242	207	203	203
query64	2808	1074	898	898
query65	4871	4794	4774	4774
query66	1773	505	386	386
query67	29471	29642	29286	29286
query68	3142	1527	1042	1042
query69	417	310	266	266
query70	1125	978	1021	978
query71	358	322	306	306
query72	3094	2888	2386	2386
query73	816	749	447	447
query74	5092	4962	4764	4764
query75	2629	2589	2261	2261
query76	2354	1186	775	775
query77	353	393	289	289
query78	12197	12322	11816	11816
query79	1415	1171	740	740
query80	1302	544	454	454
query81	523	332	282	282
query82	637	159	122	122
query83	370	318	290	290
query84	278	165	131	131
query85	965	603	531	531
query86	440	304	267	267
query87	1840	1824	1777	1777
query88	3678	2783	2780	2780
query89	460	401	362	362
query90	1972	202	196	196
query91	205	190	168	168
query92	67	62	55	55
query93	1646	1546	976	976
query94	743	368	322	322
query95	796	580	488	488
query96	1033	782	337	337
query97	2695	2682	2564	2564
query98	215	210	202	202
query99	1171	1159	1037	1037
Total cold run time: 266348 ms
Total hot run time: 180384 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.73 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit b4294bb5dba96c044c13e542a65bd4e39722204e, data reload: false

query1	0.01	0.00	0.00
query2	0.15	0.09	0.08
query3	0.37	0.25	0.26
query4	1.61	0.26	0.30
query5	0.35	0.32	0.32
query6	1.15	0.68	0.67
query7	0.04	0.00	0.00
query8	0.10	0.08	0.07
query9	0.50	0.39	0.38
query10	0.59	0.59	0.58
query11	0.31	0.18	0.18
query12	0.32	0.20	0.19
query13	0.54	0.54	0.53
query14	0.94	0.93	0.94
query15	0.68	0.58	0.61
query16	0.39	0.41	0.40
query17	1.04	1.05	1.04
query18	0.31	0.30	0.30
query19	1.94	1.80	1.83
query20	0.02	0.01	0.01
query21	15.40	0.39	0.32
query22	4.72	0.14	0.14
query23	15.82	0.50	0.30
query24	2.44	0.61	0.44
query25	0.15	0.10	0.10
query26	0.76	0.27	0.21
query27	0.11	0.09	0.10
query28	3.42	0.88	0.51
query29	12.48	4.19	3.26
query30	0.41	0.26	0.26
query31	2.78	0.61	0.33
query32	3.26	0.59	0.47
query33	2.95	3.03	2.94
query34	15.83	4.14	3.36
query35	3.32	3.34	3.30
query36	0.65	0.53	0.53
query37	0.13	0.10	0.10
query38	0.08	0.07	0.08
query39	0.08	0.07	0.06
query40	0.21	0.19	0.18
query41	0.14	0.08	0.08
query42	0.09	0.06	0.05
query43	0.07	0.07	0.06
Total cold run time: 96.66 s
Total hot run time: 25.73 s

…tests

Prove the incremental path in the regression through BE metrics: the
first append round on the duplicate-key and the merge-on-write table
must increase query_cache_stale_hit_total (the hot partition holds two
data rowsets at that point, so neither the merge-count threshold nor a
background compaction can interfere), and the delete-predicate and MoW
backfill phases must each increase
query_cache_incremental_fallback_total. Both counters only move when
enable_query_cache_incremental is on (default off, this suite is the
only one that sets it), and the assertions are one-sided deltas summed
across all backends, so concurrent suites cannot break them. Cloud mode
keeps the plain consistency checks since it always falls back by
design.

Align the suite with the regression standards: hardcoded table names,
order_qt snapshots at each stable phase backed by a generated .out, and
tables left in place after the run for debugging.

Stop discarding setup statuses in the BE UT fixture: fatal assertions
in SetUp() that print the failed Status, non-fatal checks with an early
return in create_tablet() before the tablet is registered into the
tablet map, null checks at the dereferencing call sites, and assertions
on the two previously discarded booleans in init_rs_meta().
@github-actions github-actions Bot removed the approved Indicates a PR has been approved by one committer. label Jul 13, 2026
@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 85.63% (298/348) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 57.02% (23647/41474)
Line Coverage 40.71% (231505/568712)
Region Coverage 36.60% (182957/499837)
Branch Coverage 37.72% (81788/216857)

@HappenLee

Copy link
Copy Markdown
Contributor

Nice Job, I will review BE part code

}
// Cloud tablets capture rowsets through a different (partly asynchronous)
// path; incremental merge only supports local storage for now.
if (config::is_cloud_mode()) {

@HappenLee HappenLee Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the cloud mode not support now?if we sync_rowset we can also check the right status of the tablet, we should do sync_rowset async before do the check

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloud mode is planned. The first version sticks to local storage because the approach is easiest to validate there (the delta capture and the merge-on-write bitmap check run against a view that is complete by construction), which keeps the impact of this PR contained. Once the approach is confirmed workable, I will submit a separate follow-up PR to complete cloud support.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very high level of completeness. The quality of the code output and the thoroughness of the comments are above the original code quality of Doris. Looking forward to more contributions from you.

// blocks for a write back that would be discarded anyway. Such an entry
// stays stale until compaction merges its delta away (then the capture
// above fails and a full recompute takes over).
if (decision->handle.get_cache_total_bytes() > _param.entry_max_bytes ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what circumstances would this condition be true? Is it possible that the row count is set to 10 by the user during caching, and then set to 5 by the user on the second run?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, that is the scenario. entry_max_bytes and entry_max_rows come from the session variables and are sent with every query, but they do not participate in the cache digest, so a query with a smaller limit still hits the entry written under a larger one. The write-back side already refuses to store anything above the current limits (the size accounting in cache_source_operator.cpp), so with unchanged limits a cached entry can never exceed them; the only way this condition turns true is a limit lowered after the entry was written: set in the same session, a different session using a smaller value for the same digest, or an admin lowering the global value to cap the cache memory footprint.

Without this branch such a query would still deep-copy the cached blocks into the write-back accumulation and pay one extra merge copy per delta block, only for the size accounting to clear it all once the running total crosses the limits; and since the entry can never be replaced under the new limits, copying up to the configured limit would repeat on every stale query. With the branch we skip the accumulation upfront but keep the delta scan benefit. Both directions are covered in the UTs (write_back_feasible true and false).

case TPlanNodeType::OLAP_SCAN_NODE: {
std::shared_ptr<QueryCacheRuntime> query_cache_runtime;
if (enable_query_cache) {
if (_query_cache_runtime == nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache operator should create the _query_cache_runtime before, if here is nullptr, should return error or throw exception or dcheck here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba09543. The plan tree is built in pre-order and the cache source sits above the scan, so the runtime created with it must already exist when the scan node is reached; a missing one means FE sent a malformed plan shape. Silently creating a runtime here could even drop data: with a HIT decision the scan skips scanning while no cache source emits the entry. So the lazy creation is replaced with an InternalError. The same reasoning applies to the cache source side, whose old degrade-to-pass-through branch is now an error too, and both paths are covered by unit tests (QueryCacheFragmentContextTest on the scan side, QueryCacheOperatorTest.test_missing_runtime_fails_init on the cache source side).

pool, tnode, next_operator_id(), descs, _num_instances,
enable_query_cache ? _params.fragment.query_cache_param : TQueryCacheParam {});
enable_query_cache ? _params.fragment.query_cache_param : TQueryCacheParam {},
std::move(query_cache_runtime));

@HappenLee HappenLee Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use _query_cache_runtime just like cache operator, no need a query_cache_runtime

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scan does need the shared runtime: the runtime is the single decision point for the instance. Whichever operator initializes first triggers get_or_make_decision(), and both consume the same decision object, so the pair always agrees: the scan skips scanning if and only if the cache source emits just the cached blocks (HIT), and scans only the delta if and only if the cache source merges it (INCREMENTAL). Two independent lookups are exactly the pre-existing race this PR removes: they could interleave with an eviction and write back an empty poisoned entry. The local shared_ptr copy was indeed unnecessary; ba09543 passes the member directly.

Comment thread be/src/exec/operator/olap_scan_operator.cpp Outdated
const bool cache_incremental =
_query_cache_decision != nullptr &&
_query_cache_decision->mode == QueryCacheInstanceDecision::Mode::INCREMENTAL;
const int64_t cache_start_version =

@HappenLee HappenLee Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of code is useless. Later, the line auto delta_source = _query_cache_decision->take_delta_read_source(_tablets[i].tablet->tablet_id()) already ensures that the read version is correct. Instead, this code introduces unnecessary complexity and unless i think should removed the logic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in ba09543 together with the plumbing below. The cache_incremental flag itself stays: it still gates take_delta_read_source() and keeps the parallel scanner builder off for delta scans (details in the sibling thread).

…version plumbing

Address the second round of review comments:
- replace the silent lazy creation at the scan node and the
  degrade-to-pass-through branch at the cache source with
  InternalError: a missing runtime means a malformed plan shape, and
  silently continuing could drop data on a HIT decision
- drop OlapScanner::Params::start_version and its plumbing: the
  pre-captured delta read source already determines what gets read,
  and version.first has no behavioral consumer on the BE read path
- pass the fragment's QueryCacheRuntime member directly instead of a
  local copy
- cover both fail-loud paths with unit tests
@HappenLee

Copy link
Copy Markdown
Contributor

run buildall

@HappenLee

Copy link
Copy Markdown
Contributor

/review

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.56 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit bdd1b13ec05274e07a3bc9263fe467b7cbf7f32b, data reload: false

query1	0.01	0.00	0.01
query2	0.14	0.09	0.08
query3	0.37	0.25	0.25
query4	1.61	0.25	0.25
query5	0.34	0.31	0.31
query6	1.15	0.67	0.67
query7	0.04	0.00	0.00
query8	0.09	0.07	0.08
query9	0.50	0.39	0.39
query10	0.58	0.59	0.57
query11	0.32	0.18	0.17
query12	0.31	0.18	0.19
query13	0.53	0.54	0.53
query14	0.93	0.93	0.94
query15	0.68	0.59	0.60
query16	0.39	0.39	0.39
query17	1.02	0.98	1.03
query18	0.31	0.30	0.29
query19	1.92	1.86	1.81
query20	0.01	0.01	0.02
query21	15.41	0.38	0.31
query22	4.96	0.13	0.13
query23	15.74	0.49	0.30
query24	2.49	0.57	0.44
query25	0.15	0.10	0.11
query26	0.80	0.27	0.22
query27	0.10	0.11	0.11
query28	3.58	0.90	0.53
query29	12.47	4.24	3.34
query30	0.39	0.27	0.25
query31	2.78	0.61	0.32
query32	3.23	0.61	0.47
query33	2.93	2.93	2.92
query34	15.64	4.05	3.35
query35	3.26	3.23	3.28
query36	0.66	0.53	0.51
query37	0.12	0.10	0.09
query38	0.08	0.06	0.07
query39	0.07	0.05	0.06
query40	0.20	0.19	0.16
query41	0.12	0.09	0.08
query42	0.09	0.07	0.06
query43	0.07	0.06	0.06
Total cold run time: 96.59 s
Total hot run time: 25.56 s

… deltas

Address the fourth review round on the incremental merge:

The incremental scan bypassed ParallelScannerBuilder on the assumption
that deltas are small by construction, but nothing enforces that bound:
query_cache_max_incremental_merge_count limits prior write-backs, not
the versions accumulated while an entry sits idle, so the first stale
query after a long dormant stretch can face an arbitrarily large
suffix (including cumulative-compaction rowsets wholly inside the
window) and was scanning it with one scanner per tablet. The bypass is
removed: the builder consumes the captured delta read sources exactly
as it consumes full captures (each partial read source keeps the
entire source's delete predicates and delete bitmap, and the rowset
readers are cloned per scanner), so a large suffix is split by
rowset/segment rows like any full scan, while a genuinely small delta
still collapses to a single scanner through min_rows_per_scanner,
which is the size gate the old bypass was approximating from outside.

Tests: the regression suite gains a dormant-entry phase: fill the
entry, land thirty loads on a single-tablet partition without touching
it, then assert the first stale query still takes the incremental path
and agrees with the uncached result, with
parallel_scan_min_rows_per_scanner lowered so the builder really
splits the delta into many scanners.
@asdf2014

Copy link
Copy Markdown
Member Author

run buildall

@asdf2014

Copy link
Copy Markdown
Member Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.62% (1908/2458)
Line Coverage 64.48% (34150/52966)
Region Coverage 64.91% (17575/27076)
Branch Coverage 54.06% (9419/17424)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one new issue: the new incremental session switch is forwarded to a master FE without forwarding its required enable_query_cache prerequisite, so forwarded statements silently skip query-cache normalization. The inline comment contains the concrete call path and requested fix.

Apart from that integration gap, the local-storage design is coherent: eligibility is limited to mergeable partial aggregates over supported key models, stale-entry decisions pin a complete (cached,current] snapshot, merge-on-write history rewrites force fallback, and replacement entries commit only after successful eos. I did not repeat the dormant parallel-scanner coverage gap because existing discussion discussion_r3599901507 already tracks that exact point.

Key checkpoint conclusions:

  • Goal and proof: the core incremental-hit/fallback paths are implemented and broadly covered by FE, BE, fragment-context, and regression additions, but the forwarding defect prevents the user-facing goal from being complete in a multi-FE forwarded-statement path. The supplied tests were reviewed statically, not executed under this review contract.
  • Scope and focus: the 22-file patch is focused on FE eligibility/protocol propagation, BE snapshot/runtime/cache-source behavior, metrics/configuration, and corresponding tests. No extra user focus was supplied.
  • Concurrency and lifecycle: decision publication, take-once delta-source transfer, cache-handle/rowset pinning, queue eos ordering, cancellation/error behavior, and exact-once successful writeback were traced; no additional race, deadlock, leak, or initialization-order issue survived review.
  • Configuration: the new dynamic delta-load limit has a clear zero-disable behavior and is read for new decisions; no unsafe runtime-update behavior was found.
  • Compatibility: the optional Thrift flag preserves rolling FE/BE compatibility, unsupported cloud/binlog/storage cases fall back, and no persisted metadata format changes. Session forwarding is the one incomplete compatibility boundary called out inline.
  • Parallel and fallback paths: HIT, INCREMENTAL, MISS, full scan, merge failure, compaction/path loss, delete predicates, MoW rewrites, cloud, and row-binlog paths were checked. The dormant regression's claimed multi-scanner exercise remains a known existing-thread test gap rather than a duplicate new comment.
  • Conditions and explanations: incremental authorization and storage rejection conditions are documented consistently with the implementation; future expansions must preserve the upstream merge and bitmap-version invariants.
  • Tests: additions cover FE authorization, BE decisions/operators/fragment wiring, exact/incremental/fallback results, metrics, and several negative paths. A follower-to-master forwarding test is missing, and the accepted comment requests it.
  • Observability: hit/incremental/miss/fallback metrics and runtime-profile counters expose the new decisions without an additional issue found.
  • Transactions and persistence: the feature mutates only ephemeral query-cache state; cache replacement is delayed until successful completion and leaves the old entry untouched on error/cancellation.
  • FE/BE contract: allow_incremental is optional and defaults safely on older senders/receivers; the dependent FE session-variable forwarding contract is incomplete as reported.
  • Performance: delta capture avoids holding the fragment decision lock during storage work and scans bitmap version groups; no new unsupported full-history hot-path issue was found. The previously discussed parallel-test weakness remains unresolved coverage, not a second inline finding.

Validation was static-only as required by the review bundle; no builds or tests were run. Reviewed head: 2033145942b320146968faa57bcff45814dc260b.

Comment thread fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
…lanning master

Address the fifth review round on the incremental merge:

A forwarded statement is planned by the master in a fresh ConnectContext,
which starts from the master's global values and then sees only what
getForwardVariables() sends, and query cache normalization runs wherever
the statement is planned. Only enable_query_cache_incremental was
annotated needForward, so a client that enabled both switches on a
follower reached the master with the incremental flag true while
enable_query_cache fell back to the master's global (false by default):
both planner gates then skipped cache normalization and the feature
silently did nothing. The base switch is now forwarded too, which also
fixes the same gap the base query cache had on its own before this PR:
a session-level setting, or a SET_VAR hint, never reached the planner of
a forwarded statement.

Forwarding the switch alone would have left a second gap one hop away:
QueryCacheNormalizer builds the cache param on the planning FE from
three more session variables, so with the cache now engaged on
forwarded paths a forced refresh would have been dropped and the
entries would have been sized by the master's defaults. Those three
(query_cache_force_refresh, query_cache_entry_max_bytes,
query_cache_entry_max_rows) are forwarded as well, so every variable
the query cache reads at plan time now travels with the statement. The
digest is unaffected: it is salted from the variables annotated as
affecting the query result, which needForward does not join.

Behavior change to note for multi-FE clusters: a forwarded statement now
plans with the session's query cache state instead of the master's
global one. Statements are forwarded when the follower cannot read, when
force_forward_all_queries or the debug point is on, and for every
redirected statement, which includes each non-group-commit INSERT issued
on a follower. Besides the cache itself, enable_query_cache carries the
scan-side semantics it already has locally: OlapScanNode pins the fixed
replica and, as upstream documents on skip_missing_version, that pinning
turns the missing-version rescue off. Those now apply to forwarded
statements too, exactly as they already do when the same session runs
the same statement without forwarding, so the forwarded path stops being
the odd one out.

Three honesty fixes found while reviewing the above:

The delete bitmap scan claimed the entries it reads are immutable once
their version is visible, hence race free. They are not: compaction
copies markers onto its output keeping the original version, and the
stale-rowset aggregation re-stamps a swept range onto its end version,
both at already visible versions and both on the local path. The check
is still safe, but for a different reason, now written down: it is
conservative (an in-window marker on a rowset outside the delta is read
as a rewrite), the capture endpoint check rejects a window whose path
was merged away, and what is left is the narrow non-atomic window
between capture and scan that the exact-hit path shares.

query_cache_write_back_total counts entries handed to the cache, not
entries the cache kept: with an LRU-K admission a first-seen key on a
full shard is dropped, so on a pressured BE the counter can climb while
nothing lands. The metric text says so now.

The dormant-entry regression phase never split its delta: the
per-scanner floor defaults to 2M rows, so the 210-row delta stayed with
one scanner and the phase covered none of the splitting it was written
for. It now lowers the floor to the reachable minimum of 1024 rows (BE
clamps the variable up to that) and grows the delta to 6000 rows across
30 rowsets, which the builder cuts into four or five scanners.

Tests: SessionVariablesTest covers the forwarding contract end to end,
setting all five variables on a follower-side session, sending them
through getForwardVariables() and asserting a fresh master-side session
receives every one of them instead of its defaults.
@asdf2014

Copy link
Copy Markdown
Member Author

run buildall

@asdf2014

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: one P1 wrong-result race remains in the merge-on-write incremental-cache decision.

Critical checkpoint conclusions

  • Goal and proof: The FE-to-BE flow selects an eligible non-final partial aggregation, carries the optional incremental flag, captures delta rowsets, merges cached and delta aggregate states, and writes the refreshed entry. The changed FE, BE, and regression tests exercise authorization, stale hits, designed fallbacks, forwarding, write-back, and result equivalence. The append-only path is supported, but the MoW path is not correct under the compaction/GC interleaving in the inline finding, and no test covers it.
  • Scope and focus: The 23 changed files are focused on incremental-cache eligibility, decision/runtime plumbing, pipeline integration, configuration/metrics, and tests. The implementation appropriately reuses the existing query-cache and scan pipelines, but its correctness depends on storage bitmap lifetime that the change does not preserve.
  • Concurrency and locks: Fragment decision publication moves metadata work outside the fragment map mutex, and I found no reverse lock-order or deadlock issue in the new path. The blocker is the non-atomic sequence between version-path capture and live delete-bitmap inspection: compaction, stale sweeping, and unused-rowset GC can remove cached-side evidence because delta capture pins only delta rowsets.
  • Lifecycle and memory: Cache handles, captured read sources, queue dependencies, COW block ownership, entry-size rejection, error/eos/limit exits, and exactly-once write-back were reviewed; no cycle, static-initialization, or additional release issue was found. The unresolved lifecycle defect is specifically the cached-side delete marker described inline.
  • Configuration and compatibility: The mutable merge-count threshold is read at decision time and non-positive values conservatively force full recomputation. The new Thrift field is optional and guarded by __isset; all planner-read session variables are forwarded, old FE/BE peers degrade safely, cloud and binlog paths are disabled, and there is no storage-format or EditLog change.
  • Parallel paths and conditions: DUP_KEYS is append-only for this purpose. MoR UNIQUE, AGG, cloud, invalid-key, and binlog paths fall back; scan-range identity and per-instance source consumption are consistent. Local MoW is the only accepted correctness issue. No analogous missed change was found in the other guarded paths.
  • Tests and results: I inspected the FE/BE unit tests, regression suite, and generated result file, including negative/fallback and dormant-delta cases. They were not executed because this review environment explicitly prohibits builds and test runs. A deterministic sync-point test for capture followed by boundary-crossing compaction, immediate stale sweep, and unused-rowset GC is still required.
  • Observability and performance: The new hit/fallback/write-back metrics and diagnostic reasons cover the critical outcomes; the bounded bitmap-group walk and out-of-lock decision build address the previously raised performance concerns. No additional observability or performance blocker was found.
  • Transactions, persistence, and data writes: The feature writes only ephemeral cache state and changes no Doris table data, transaction protocol, persisted metadata schema, or replay path. The accepted issue is nevertheless a read-snapshot correctness failure that can publish a wrong cache entry.
  • Other review checks: Status/error handling, ownership, version bounds, nullable handling, and deterministic test-output conventions were checked where applicable. After deduplication against all existing threads and three convergence rounds, no other valuable finding remains.

Comment thread be/src/runtime/query_cache/query_cache.cpp
@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 92.00% (23/25) 🎉
Increment coverage report
Complete coverage report

…rite delta

Address the P1 on the incremental merge decision.

The delta capture pins only the delta rowsets, so on a merge-on-write
table the rewrite markers classification looks for have no other owner.
Those markers live on the rowsets the cached snapshot was built from,
and a compaction spanning the cached and delta versions retires such a
rowset without relocating an overwritten row's marker: the row has no
counterpart in the compaction output, so
calc_compaction_output_rowset_delete_bitmap skips it and the marker
stays on the retired input. The stale sweep then hands that input to
the unused-rowset GC, which drops its delete bitmap. If that lands
between the delta capture and the bitmap scan, _delta_rewrites_history
sees no rewrite, the cached aggregate is merged with the rows that
replaced it, and a wrong entry is stored at the current version and
served by later exact hits.

The classification now pins the cached side first: under the tablet
header lock it captures the rowsets of [0, cached_version] and holds
them across _delta_rewrites_history. start_delete_unused_rowset()
collects a retired rowset only once nothing else references it, and
only a collected rowset reaches remove_rowset_delete_bitmap(), so the
reference kept here keeps the evidence too. A quiet capture returns the
prefix it walked, so the pinned path is checked to cover the whole
[0, cached_version] range; a short prefix (its own history compacted
away and swept) falls back to a full recompute rather than classifying
against evidence it cannot vouch for.

Tests: two cases against a real storage engine. One pins the cached
side across a merge-on-write classification and asserts, through a sync
point, that the full [0, cached_version] range is held at that moment;
the other gives the cached side a hole so the quiet capture returns a
short prefix and asserts the coverage check falls back. Both fail on
the pre-fix code (the first because the pin is not held, the second
because the incremental path is taken instead of the fallback).
@asdf2014

Copy link
Copy Markdown
Member Author

run buildall

@asdf2014

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/29596419501

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 92.00% (23/25) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29821 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 789c8b4158f2e891c26425828bac114c13256cbb, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17646	4261	4330	4261
q2	2020	342	194	194
q3	10813	1467	848	848
q4	4809	469	336	336
q5	8351	869	568	568
q6	319	174	140	140
q7	862	804	619	619
q8	10525	1614	1451	1451
q9	5875	4411	4415	4411
q10	6781	1748	1476	1476
q11	521	350	323	323
q12	736	591	465	465
q13	18127	3336	2755	2755
q14	269	258	250	250
q15	q16	794	769	710	710
q17	1002	1081	900	900
q18	6789	5800	5548	5548
q19	1179	1287	1094	1094
q20	770	694	622	622
q21	5661	2769	2554	2554
q22	435	358	296	296
Total cold run time: 104284 ms
Total hot run time: 29821 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4525	4512	4454	4454
q2	281	327	208	208
q3	4595	5055	4410	4410
q4	2083	2159	1354	1354
q5	4500	4339	4563	4339
q6	266	199	150	150
q7	2042	1808	1616	1616
q8	2761	2203	2132	2132
q9	7851	7810	7655	7655
q10	4713	4736	4207	4207
q11	573	445	433	433
q12	744	764	535	535
q13	3364	3674	2970	2970
q14	297	301	286	286
q15	q16	702	743	655	655
q17	1389	1389	1342	1342
q18	8062	7494	6962	6962
q19	1130	1097	1095	1095
q20	2223	2217	1924	1924
q21	5318	4643	4415	4415
q22	507	484	422	422
Total cold run time: 57926 ms
Total hot run time: 51564 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177006 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 789c8b4158f2e891c26425828bac114c13256cbb, data reload: false

query5	4312	636	485	485
query6	462	234	215	215
query7	4913	628	347	347
query8	344	194	174	174
query9	8776	4136	4124	4124
query10	465	349	306	306
query11	5868	2316	2130	2130
query12	155	103	100	100
query13	1312	592	421	421
query14	6242	5282	5005	5005
query14_1	4334	4292	4335	4292
query15	220	210	180	180
query16	1051	474	483	474
query17	1139	748	588	588
query18	2597	486	351	351
query19	208	192	151	151
query20	114	107	104	104
query21	234	164	139	139
query22	13598	13635	13267	13267
query23	17240	16580	16049	16049
query23_1	16246	16229	16289	16229
query24	7519	1759	1279	1279
query24_1	1312	1267	1318	1267
query25	566	453	383	383
query26	1331	333	210	210
query27	2587	555	403	403
query28	4451	1986	2006	1986
query29	1096	616	488	488
query30	343	272	232	232
query31	1115	1108	980	980
query32	121	63	62	62
query33	546	336	264	264
query34	1153	1158	666	666
query35	788	789	703	703
query36	1166	1226	1054	1054
query37	167	101	92	92
query38	1876	1707	1604	1604
query39	902	870	846	846
query39_1	878	821	826	821
query40	257	156	143	143
query41	65	63	61	61
query42	94	89	88	88
query43	328	334	286	286
query44	1482	774	766	766
query45	200	190	174	174
query46	1070	1212	701	701
query47	2143	2136	2000	2000
query48	402	413	293	293
query49	600	416	300	300
query50	1040	423	325	325
query51	10702	10692	10602	10602
query52	82	85	75	75
query53	258	286	201	201
query54	277	223	222	222
query55	73	75	64	64
query56	284	286	270	270
query57	1339	1293	1198	1198
query58	296	256	271	256
query59	1576	1645	1474	1474
query60	295	280	258	258
query61	154	148	142	142
query62	539	501	427	427
query63	248	199	211	199
query64	2791	1048	879	879
query65	4729	4626	4638	4626
query66	1786	500	387	387
query67	29309	28656	29021	28656
query68	3053	1590	939	939
query69	388	301	302	301
query70	1047	957	978	957
query71	371	331	303	303
query72	3094	2690	2353	2353
query73	821	781	434	434
query74	5095	4949	4708	4708
query75	2555	2512	2118	2118
query76	2364	1169	777	777
query77	357	376	274	274
query78	11956	11864	11280	11280
query79	1399	1141	758	758
query80	1293	563	460	460
query81	535	341	289	289
query82	594	157	120	120
query83	383	319	298	298
query84	295	159	135	135
query85	969	578	509	509
query86	421	293	274	274
query87	1831	1825	1767	1767
query88	3785	2790	2751	2751
query89	441	366	337	337
query90	1911	208	204	204
query91	202	190	157	157
query92	62	62	56	56
query93	1727	1530	911	911
query94	711	375	315	315
query95	789	494	490	490
query96	1051	850	347	347
query97	2652	2601	2478	2478
query98	217	208	201	201
query99	1091	1110	986	986
Total cold run time: 264008 ms
Total hot run time: 177006 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.62 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 789c8b4158f2e891c26425828bac114c13256cbb, data reload: false

query1	0.00	0.00	0.01
query2	0.14	0.08	0.09
query3	0.38	0.24	0.24
query4	1.61	0.25	0.24
query5	0.33	0.32	0.32
query6	1.16	0.68	0.66
query7	0.04	0.01	0.01
query8	0.09	0.07	0.07
query9	0.50	0.38	0.38
query10	0.58	0.58	0.59
query11	0.32	0.18	0.18
query12	0.32	0.19	0.18
query13	0.52	0.52	0.52
query14	0.94	0.92	0.91
query15	0.68	0.60	0.59
query16	0.38	0.39	0.39
query17	1.00	1.01	1.01
query18	0.30	0.29	0.29
query19	1.91	1.82	1.85
query20	0.02	0.01	0.02
query21	15.41	0.37	0.34
query22	4.90	0.14	0.14
query23	15.83	0.49	0.30
query24	2.49	0.60	0.43
query25	0.15	0.11	0.10
query26	0.76	0.27	0.21
query27	0.11	0.10	0.09
query28	3.40	0.88	0.51
query29	12.49	4.20	3.37
query30	0.38	0.27	0.26
query31	2.76	0.62	0.33
query32	3.23	0.61	0.47
query33	2.95	2.95	2.97
query34	15.64	4.01	3.32
query35	3.30	3.27	3.25
query36	0.65	0.53	0.51
query37	0.12	0.09	0.10
query38	0.08	0.06	0.07
query39	0.07	0.06	0.06
query40	0.20	0.19	0.17
query41	0.12	0.08	0.07
query42	0.09	0.06	0.05
query43	0.07	0.06	0.07
Total cold run time: 96.42 s
Total hot run time: 25.62 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 96.86% (370/382) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.95% (30479/40666)
Line Coverage 59.10% (335986/568500)
Region Coverage 55.79% (281647/504839)
Branch Coverage 57.08% (124967/218926)

@asdf2014

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed: I found no new actionable issue beyond the concerns already raised and addressed in the existing review threads.

  • Goal and proof: the change implements experimental, default-off incremental reuse of stale local query-cache entries by combining a pinned cached partial-aggregation result with a captured delta scan. The FE/BE unit tests and regression additions cover eligibility, exact hits, incremental hits, conservative fallbacks, MoW history rewrites, failure/writeback behavior, forwarding, and large-delta scanner parallelism.
  • Scope and focus: the 23-file change is focused on query-cache planning, runtime/storage capture, pipeline operators, metrics/configuration, protocol plumbing, and their tests. No extra user focus was provided; the full changed-file set was reviewed.
  • Concurrency: fragment-instance decisions are published once per sorted tablet key, non-empty instance ranges are disjoint, and each tablet delta source has one scan-local-state consumer before internal scanner splitting. Storage capture, compaction marker relocation, and delete-bitmap inspection use the existing header/update/bitmap locks without a newly identified lock inversion or heavy operation under the fragment decision mutex.
  • Lifecycle, errors, and memory: rowset readers and the old cache handle pin all evidence/data through classification and consumption. Cached columns remain read-only COW views until one tracked deep materialization. Only successfully drained EOS can publish a replacement once; initialization failure, merge failure, cancellation, and limit rejection leave the old entry intact. No static-initialization or ownership-cycle issue was introduced.
  • Configuration, compatibility, and parallel paths: the new switch is dynamic and defaults off; the merge threshold is a conservative dynamic fallback policy. Optional appended thrift field 8 is safe for rolling FE/BE combinations, all five planning variables are forwarded, and legacy/Nereids assignment plus ordinary, streaming, distinct, and spill aggregation paths were checked. Unsupported cloud, MOR/AGG, delete-predicate, gapped-version, binlog, and rewrite cases conservatively recompute.
  • Conditions, persistence, and data correctness: FE enables incremental reuse only for direct-scan non-finalize mergeable states over the selected DUP or MoW UNIQUE index, and BE revalidates the actual tablet. The feature does not add persisted metadata, EditLog state, or transaction writes; cache replacement is local and atomic at the LRU entry boundary. FE-to-BE passing is covered by the optional thrift flag and forwarded session variables.
  • Tests, results, and observability: the changed expected regression output is consistent with the deterministic counter/result checks in the new suite. Positive, negative, boundary, concurrency, and failure-focused tests are present, and stale hits, fallbacks, writebacks, reasons, and profile counters provide the needed observability.
  • Performance and remaining risks: large deltas retain parallel reader splitting, cache writes materialize once, and configured entry limits bound admitted entries. The temporary old-plus-new replacement peak and compaction/GC interleaving request were already discussed in existing threads. The implementation is covered by direct pin/fallback invariant tests, but the actual timed compaction+sweep+GC interleaving was not orchestrated; no distinct performance or correctness defect was substantiated.

Validation was static only, as required by the review runner instructions; I did not run a local build or tests.

@HappenLee HappenLee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Jul 18, 2026
@HappenLee
HappenLee merged commit a94d7b8 into apache:master Jul 18, 2026
31 of 33 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@asdf2014
asdf2014 deleted the query-cache-incremental-merge branch July 18, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants