Skip to content

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

Open
asdf2014 wants to merge 10 commits into
apache:masterfrom
asdf2014:query-cache-incremental-merge
Open

[feature](query-cache) Support incremental merge for stale query cache entries#65482
asdf2014 wants to merge 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
TPC-DS: Total hot run time: 176682 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 b73ecf779463a3abee4d71c898397981e5fde932, data reload: false

query5	4330	640	487	487
query6	470	241	200	200
query7	4842	634	342	342
query8	347	193	174	174
query9	8764	4114	4089	4089
query10	477	355	298	298
query11	5836	2340	2124	2124
query12	155	104	103	103
query13	1287	602	418	418
query14	6239	5225	4878	4878
query14_1	4265	4254	4282	4254
query15	213	207	180	180
query16	1020	473	480	473
query17	1124	711	537	537
query18	2489	463	344	344
query19	203	181	138	138
query20	108	109	108	108
query21	232	155	132	132
query22	13518	13618	13360	13360
query23	17177	16462	16066	16066
query23_1	16270	16207	16061	16061
query24	7669	1757	1268	1268
query24_1	1274	1268	1268	1268
query25	529	416	351	351
query26	1326	341	204	204
query27	2650	588	363	363
query28	4451	1978	1982	1978
query29	1053	608	459	459
query30	336	263	236	236
query31	1111	1089	980	980
query32	101	61	59	59
query33	500	305	242	242
query34	1156	1149	602	602
query35	763	777	673	673
query36	1167	1210	1050	1050
query37	153	106	91	91
query38	1876	1706	1614	1614
query39	895	872	842	842
query39_1	872	832	838	832
query40	251	169	149	149
query41	71	68	70	68
query42	95	91	91	91
query43	329	319	279	279
query44	1487	777	774	774
query45	206	189	182	182
query46	1088	1186	709	709
query47	2125	2177	2011	2011
query48	421	411	307	307
query49	588	424	313	313
query50	1055	421	321	321
query51	10847	10427	10861	10427
query52	86	96	75	75
query53	259	284	202	202
query54	300	257	239	239
query55	81	72	70	70
query56	305	325	317	317
query57	1329	1328	1199	1199
query58	292	247	252	247
query59	1589	1666	1428	1428
query60	304	269	257	257
query61	153	148	145	145
query62	541	488	436	436
query63	233	200	209	200
query64	2803	1011	849	849
query65	4715	4586	4576	4576
query66	1796	491	380	380
query67	29223	29159	29064	29064
query68	3340	1474	909	909
query69	409	298	255	255
query70	1051	989	962	962
query71	373	328	366	328
query72	3037	2904	2381	2381
query73	801	773	440	440
query74	5070	4956	4726	4726
query75	2523	2507	2133	2133
query76	2332	1185	775	775
query77	373	377	285	285
query78	11980	11957	11268	11268
query79	1234	1176	763	763
query80	668	563	459	459
query81	460	339	285	285
query82	247	155	123	123
query83	403	332	300	300
query84	278	155	129	129
query85	924	611	519	519
query86	334	294	258	258
query87	1808	1822	1773	1773
query88	3759	2792	2729	2729
query89	429	382	328	328
query90	2060	211	202	202
query91	204	193	161	161
query92	61	58	56	56
query93	1670	1548	977	977
query94	556	357	304	304
query95	788	514	584	514
query96	1055	835	345	345
query97	2624	2614	2497	2497
query98	216	205	200	200
query99	1090	1111	971	971
Total cold run time: 262213 ms
Total hot run time: 176682 ms

@hello-stephen

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

query1	0.00	0.00	0.01
query2	0.14	0.09	0.10
query3	0.36	0.24	0.24
query4	1.61	0.25	0.25
query5	0.33	0.32	0.31
query6	1.15	0.66	0.67
query7	0.03	0.01	0.01
query8	0.09	0.07	0.08
query9	0.51	0.39	0.38
query10	0.57	0.58	0.59
query11	0.30	0.18	0.18
query12	0.30	0.18	0.18
query13	0.52	0.53	0.51
query14	0.92	0.92	0.91
query15	0.67	0.59	0.60
query16	0.39	0.39	0.39
query17	1.00	0.99	0.95
query18	0.30	0.29	0.29
query19	1.88	1.80	1.83
query20	0.02	0.01	0.02
query21	15.40	0.38	0.32
query22	4.70	0.14	0.13
query23	15.82	0.49	0.30
query24	2.44	0.62	0.42
query25	0.15	0.11	0.09
query26	0.74	0.28	0.21
query27	0.11	0.10	0.10
query28	3.51	0.88	0.51
query29	12.51	4.22	3.33
query30	0.37	0.25	0.26
query31	2.76	0.59	0.33
query32	3.23	0.60	0.47
query33	2.91	2.94	2.94
query34	15.78	4.11	3.37
query35	3.28	3.24	3.29
query36	0.62	0.53	0.52
query37	0.12	0.09	0.09
query38	0.08	0.07	0.06
query39	0.08	0.06	0.06
query40	0.20	0.18	0.17
query41	0.13	0.08	0.08
query42	0.08	0.06	0.06
query43	0.07	0.06	0.06
Total cold run time: 96.18 s
Total hot run time: 25.51 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 96.70% (352/364) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.98% (30492/40667)
Line Coverage 59.13% (336115/568457)
Region Coverage 55.81% (281739/504784)
Branch Coverage 57.08% (124906/218826)

…napshot cached blocks zero-copy

Address the third review round on the incremental merge:

Entry commit: the write-back entry was published from an unconditional
Defer that also ran during error unwinding. On the final delta block
eos is observed BEFORE the block is merged, so a merge failure there
(allocation failure surfaces as a non-OK Status through
RETURN_IF_CATCH_EXCEPTION) published an entry that was missing the
failed block under the current version, and a later exact hit would
serve the incomplete result silently. The commit now happens only at
the two explicit success exits (the empty pull that observes eos, and
the exit where the pulled block was fully consumed), so every error
return leaves the cache untouched and the pinned stale entry stays the
served truth. This also closes the exception-unwind exit (the old
Defer ran the insert inside a destructor while unwinding, which could
std::terminate if the insert itself threw), and the insert is latched
exactly-once, so even an out-of-contract extra poll after eos cannot
replace the committed entry with an empty one.

Write-back copies: replacing an entry peaked at roughly three payloads
(the pinned old entry, the deep-copied write-back set, and the
insert-time copy), and the LRU accounting could not see the pinned one
once the same-key insert displaced it. The cached blocks are now
snapshotted as zero-copy views sharing the COW column pointers of the
pinned entry, and the single materialization stays in
QueryCache::insert, so a replacement peaks at the old entry plus the
new entry. Delta blocks were already moved and are unchanged.

Tests: a new case injects a deterministic merge failure on the final
delta block and asserts the query fails, nothing is published at the
current version, and the stale entry is untouched; the reordered
write-back case now pins the column sharing before insert, the single
materialization after insert, and that an extra poll after eos keeps
the committed entry intact; test_no_hit_cache1 asserts the latched
flag, with the element count as the insert witness.
@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.

Automated review completed; requesting changes for one performance issue.

  • Goal, scope, and clarity: the PR adds an opt-in stale query-cache path that combines a pinned cached partial-aggregation result with an exactly captured tablet-version suffix. The FE eligibility checks, BE storage revalidation, and conservative full-recompute fallbacks accomplish that goal for the reviewed correctness paths, and the change remains focused on query-cache planning, execution, storage validation, observability, and tests.
  • Concurrency and locks: scan/cache-source local states can initialize concurrently, but they publish one immutable fragment decision under a narrow mutex and transfer each tablet read source once under a separate take lock. Tablet header/delete-bitmap locking and the existing queue synchronization have consistent scopes; heavy capture and cache work remain outside the fragment critical section. I found no deadlock or unsafe atomic/lifecycle signal.
  • Lifecycle, error handling, and memory safety: cache handles and rowset readers pin their data through consumption, COW block views remain immutable until the single tracked cache materialization, and replacement occurs exactly once only after successful queue EOS and merge/accounting. Error, cancellation, limit, and failed-merge paths retain the old entry. No ownership cycle, abnormal release, or cross-translation-unit static-initialization dependency was found.
  • Configuration and compatibility: the merge-count threshold is mutable and read for each decision; the experimental session switch follows the existing forwarded-variable path. The new Thrift field is optional and BE checks __isset, preserving mixed FE/BE behavior. Cloud, binlog, force-refresh, unsupported key modes, delete predicates, and unsafe MOW-history paths conservatively fall back.
  • Parallel and conditional paths: exact hit, miss, incremental, publication-race, DUP, MOW, one-/two-phase aggregation, finalized/nested/distinct, selected-index, and replacement paths were traced. The remaining issue is the unconditional incremental-scan exception to ParallelScannerBuilder: the code assumes a small delta without enforcing any size bound.
  • Tests and results: FE, BE runtime/operator/fragment, and end-to-end regression coverage exercise enablement, safe stale hits, merge-count rebasing, races, one-shot ownership, slot permutation, write-back/error behavior, delete predicates, and MOW history fallback. The generated aggregate results were checked against the SQL. No test covers a long-dormant cache entry followed by a large suffix, which is the requested coverage for the inline issue.
  • Observability: stale-hit, fallback, and write-back counters are registered and updated on winning paths, while profiles retain hit state, delta window, fallback reason, and insertion state. No additional observability defect was found.
  • Persistence, transactions, and data writes: this changes only the process-local read cache; it adds no storage format, EditLog, durable transaction, or table-write path. Failed refreshes leave the prior cache entry intact.
  • FE-BE propagation: the eligibility bit is serialized in the query-cache parameter on the fragment path and is guarded for older senders; the session control is forwarded through the existing annotated mechanism. No missing parallel sending path was found.
  • Performance: MM-1 must be addressed because a dormant entry can accumulate an unbounded suffix while still bypassing intra-tablet rowset/segment parallelism.
  • User focus: no additional review focus was supplied; the whole PR was reviewed.

Validation was static only, as required by this review runner; no build or test command was executed here.

Comment thread be/src/exec/operator/olap_scan_operator.cpp Outdated
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17684	4068	4086	4068
q2	2031	336	197	197
q3	10784	1435	804	804
q4	4727	471	336	336
q5	8090	861	576	576
q6	297	176	136	136
q7	786	827	619	619
q8	10704	1675	1746	1675
q9	5820	4383	4325	4325
q10	6788	1746	1451	1451
q11	503	346	330	330
q12	728	588	474	474
q13	18165	3406	2782	2782
q14	277	266	245	245
q15	q16	793	786	707	707
q17	1211	1010	994	994
q18	6854	5670	5539	5539
q19	1566	1351	1074	1074
q20	821	657	583	583
q21	5863	2657	2395	2395
q22	434	361	301	301
Total cold run time: 104926 ms
Total hot run time: 29611 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4520	4414	4397	4397
q2	294	328	230	230
q3	4580	5122	4432	4432
q4	2057	2166	1363	1363
q5	4482	4303	4646	4303
q6	263	216	156	156
q7	2038	1812	1654	1654
q8	2525	2282	2200	2200
q9	7942	7786	7818	7786
q10	4689	4697	4273	4273
q11	605	431	411	411
q12	758	746	546	546
q13	3320	3653	2978	2978
q14	301	300	282	282
q15	q16	724	755	649	649
q17	1403	1372	1361	1361
q18	8152	7341	6742	6742
q19	1103	1113	1074	1074
q20	2217	2199	1951	1951
q21	5288	4600	4505	4505
q22	515	461	404	404
Total cold run time: 57776 ms
Total hot run time: 51697 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177382 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 bdd1b13ec05274e07a3bc9263fe467b7cbf7f32b, data reload: false

query5	4325	633	487	487
query6	480	234	200	200
query7	4841	618	350	350
query8	344	198	179	179
query9	8759	4087	4046	4046
query10	490	376	325	325
query11	5763	2335	2142	2142
query12	157	104	104	104
query13	1334	589	434	434
query14	6252	5257	4923	4923
query14_1	4319	4326	4251	4251
query15	208	202	180	180
query16	1028	486	445	445
query17	1125	723	576	576
query18	2480	487	349	349
query19	218	192	159	159
query20	113	114	105	105
query21	231	163	137	137
query22	13616	13450	13358	13358
query23	17360	16438	16130	16130
query23_1	16230	16175	16234	16175
query24	7465	1765	1295	1295
query24_1	1307	1297	1277	1277
query25	582	458	400	400
query26	1332	360	215	215
query27	2579	556	378	378
query28	4459	1997	1986	1986
query29	1080	618	499	499
query30	350	257	232	232
query31	1118	1095	974	974
query32	111	64	63	63
query33	531	324	253	253
query34	1231	1172	664	664
query35	758	803	667	667
query36	1193	1188	1071	1071
query37	160	109	94	94
query38	1887	1713	1631	1631
query39	884	871	871	871
query39_1	841	836	835	835
query40	258	176	179	176
query41	64	65	66	65
query42	90	92	90	90
query43	326	324	276	276
query44	1401	747	751	747
query45	197	186	174	174
query46	1106	1159	746	746
query47	2151	2060	1987	1987
query48	405	432	272	272
query49	572	410	305	305
query50	1031	415	340	340
query51	10845	10872	10637	10637
query52	88	86	75	75
query53	268	268	200	200
query54	289	237	222	222
query55	75	71	65	65
query56	290	313	291	291
query57	1306	1307	1198	1198
query58	323	256	252	252
query59	1581	1658	1424	1424
query60	309	269	261	261
query61	155	147	145	145
query62	543	497	434	434
query63	238	198	200	198
query64	2838	1052	886	886
query65	4726	4646	4600	4600
query66	1812	493	378	378
query67	29243	29220	29037	29037
query68	3147	1580	986	986
query69	404	304	268	268
query70	1034	945	953	945
query71	397	351	344	344
query72	3074	2664	2344	2344
query73	851	772	426	426
query74	5023	4948	4726	4726
query75	2510	2487	2130	2130
query76	2320	1166	772	772
query77	348	370	272	272
query78	11800	11778	11293	11293
query79	1412	1154	768	768
query80	1306	583	478	478
query81	535	333	288	288
query82	593	155	118	118
query83	386	323	299	299
query84	286	159	135	135
query85	976	634	511	511
query86	422	296	282	282
query87	1834	1818	1759	1759
query88	3693	2805	2758	2758
query89	428	375	327	327
query90	1940	191	193	191
query91	203	188	163	163
query92	62	61	55	55
query93	1630	1484	917	917
query94	743	352	318	318
query95	793	499	556	499
query96	1028	815	365	365
query97	2623	2652	2511	2511
query98	216	206	201	201
query99	1091	1116	987	987
Total cold run time: 263575 ms
Total hot run time: 177382 ms

@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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants