Skip to content

[fix](exec) Retain sliding window rows during eviction - #67274

Merged
HappenLee merged 2 commits into
apache:masterfrom
Mryange:fix-analytic-sliding-rows-eviction
Aug 31, 2026
Merged

[fix](exec) Retain sliding window rows during eviction#67274
HappenLee merged 2 commits into
apache:masterfrom
Mryange:fix-analytic-sliding-rows-eviction

Conversation

@Mryange

@Mryange Mryange commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Problem Summary: Streaming ROWS window aggregates retain state across frame evaluations. The eviction path previously considered only whether buffered blocks had been emitted, so it could erase either the outgoing row needed by a bounded sliding frame or the next unread row needed by an UNBOUNDED PRECEDING ... N PRECEDING frame. After rebasing, negative partition and outgoing positions could allow a nullable aggregate to access its null map out of bounds. Evicting either kind of required row could also produce incorrect aggregate results.

Root cause: _remove_unused_rows() did not account for the earliest row required by the next ROWS frame evaluation, and BoundaryPose::remove_unused_rows() allowed retained-column coordinates to become negative.

This change defers block-aligned eviction when the candidate prefix contains either frame_start - 1, the outgoing row required by a bounded sliding update, or the next unread row required by an UNBOUNDED PRECEDING ... N PRECEDING frame. It also rebases partition and order boundaries to nonnegative physical-column coordinates. The BE unit coverage exercises both ROWS executors across eviction boundaries and verifies boundary rebasing.

Observed ASAN failure before this change (output/be/log/be.out):

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
    #0 doris::AggregateFunctionNullUnaryInlineV2<...>::execute_function_with_incremental(...)
       be/src/exprs/aggregate/aggregate_function_null_v2.h:595
    #1 doris::AggFnEvaluator::execute_function_with_incremental(...)
       be/src/exprs/vectorized_agg_fn.cpp:334
    #2 doris::AnalyticSinkLocalState::_execute_for_function<true>(...)
       be/src/exec/operator/analytic_sink_operator.cpp:385
    #3 doris::AnalyticSinkLocalState::_get_next_for_sliding_rows(...)
       be/src/exec/operator/analytic_sink_operator.cpp:203
    #4 doris::AnalyticSinkLocalState::_execute_impl(...)
       be/src/exec/operator/analytic_sink_operator.cpp:358
    #5 doris::AnalyticSinkOperatorX::sink_impl(...)
       be/src/exec/operator/analytic_sink_operator.cpp:757
SUMMARY: AddressSanitizer: heap-buffer-overflow in
doris::AggregateFunctionNullUnaryInlineV2<...>::execute_function_with_incremental(...)

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

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

### What problem does this PR solve?

Issue Number: N/A

Related PR: N/A

Problem Summary: Streaming analytic execution could erase rows that a bounded ROWS frame still needed for the next incremental update. Rebased partition and frame coordinates could then become negative, causing nullable aggregates to access the null map out of bounds or non-nullable aggregates to return incorrect results. Preserve the earliest row required by the next sliding frame before evicting buffered data, and clamp rebased boundary coordinates to the retained columns.

### Release note

Fix potential crashes and incorrect results for bounded sliding ROWS window aggregates when streaming input rows are evicted.

### Check List (For Author)

- Test: Not run; compilation and test execution were deferred for review.
    - Added unit coverage for eviction across a bounded ROWS frame and nonnegative boundary rebasing.
    - clang-format, diff checks, and BE build hygiene passed.
- Behavior changed: Yes. Sliding ROWS eviction retains rows required by the next incremental frame.
- Does this need documentation: No
@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?

@Mryange

Mryange commented Aug 28, 2026

Copy link
Copy Markdown
Contributor 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

The bounded sliding ROWS calculation and both changed test expectations are correct, but the fix does not cover the parallel unbounded-start ROWS executor. That path can still delete unread input and return wrong analytic results once streaming reclamation starts; the blocking trace and fix direction are inline.

Critical checkpoints

  • Goal and proof: The new guard correctly retains frame_start - 1 for bounded sliding frames, the widened sum test fails the base wrong-result path and proves later safe reclamation, and the BoundaryPose test correctly proves saturation. The overall reclamation goal is incomplete because UNBOUNDED PRECEDING ... N PRECEDING bypasses the new flag and guard.
  • Scope, clarity, and parallel conditions: The three-file patch is small and the bounded-frame formula is exact, conservative only for fallback aggregates, and eventually permits deletion. The executor-shape flag is too narrow: the omitted-start PRECEDING executor has the same next-read retention requirement. CURRENT/FOLLOWING, whole-partition, bounded-RANGE, repeated rebase, partition reset, and EOS paths were traced without another in-scope defect.
  • Concurrency and lifecycle: Reclamation, frame coordinates, and aggregate state are sink-local and mutate before input append/evaluation. Existing output-buffer and EOS locks are unchanged; no new thread, lock-order, static-initialization, ownership, or teardown issue was found.
  • Configuration, compatibility, persistence, and writes: No configuration, thrift/wire symbol, persisted format, transaction, EditLog, data-write, cloud split, or rolling-upgrade behavior changes.
  • Tests and results: The modified sums {0,1,3,6,10,15,20,25,30,35} and pose {0,3} are correct. Existing tests do not exercise the omitted-start PRECEDING reclamation failure. No build, unit test, or regression test was run by this automated review, as required by the review prompt; the pure-text header-hygiene gate passed.
  • Errors, memory, and nullability: No Status, exception, allocation, tracker, or error-recovery behavior changes. The bounded retention check is pre-dispatch and type-independent, so an additional nullable reproduction would be useful hardening but is not a separate blocker.
  • Performance and observability: The new work is constant-time per reclamation attempt with no allocation or rescan. Existing removal counters, timer, and block-memory high-water mark remain sufficient; no new metric or log is required.

Follow-up observations, not blockers for this PR: base and head both retain stale physical row identity for first_value/nth_value (and retained last_value IGNORE NULLS) across in-place prefix erase, and stale peer coordinates for streaming rank/dense_rank. Neither behavior is caused or worsened by this bounded-ROWS patch, so they should be tracked separately rather than attached as blocking inline comments here.

User focus: No additional focus was supplied.

Review status: Complete after three rounds. All Round 3 reviewers returned NO_NEW_VALUABLE_FINDINGS; every candidate was accepted, deduplicated, or dismissed with evidence.

_executor.get_next_impl = &AnalyticSinkLocalState::_get_next_for_unbounded_rows;
} else {
_executor.get_next_impl = &AnalyticSinkLocalState::_get_next_for_sliding_rows;
_is_sliding_rows = true;

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 flag is not set for ROWS BETWEEN UNBOUNDED PRECEDING AND 5 PRECEDING, so the new retention gate still lets _get_next_for_unbounded_rows() lose its next unread row. With two-row BE_TEST blocks containing 1-8, before row 6 the state contains only row 0/value 1 and the next evaluation must add row 1/value 2, but cutoff 2 erases rows 0-1. After rebasing, the add range clips empty and row 6 returns 1 instead of 3. Production has the same wrong-result path when the PRECEDING offset exceeds the 256-block reclamation distance. Please extend retention to this executor (preserving max(partition_start, current + rows_end_offset)) and add a focused regression.

### What problem does this PR solve?

Issue Number: N/A

Problem Summary: Streaming reclamation for ROWS windows protected bounded sliding frames but could still erase the next unread row required by an UNBOUNDED PRECEDING to N PRECEDING frame. Track the ROWS executor shape and retain the earliest row required by either bounded sliding or unbounded-start evaluation.

### Release note

Fix incorrect results for unbounded-start ROWS window aggregates during streaming input eviction.

### Check List (For Author)

- Test: Not run; compilation and test execution were deferred for review.
    - Added unit coverage for unbounded-start ROWS eviction.
    - clang-format, diff checks, and BE build hygiene passed.
- Behavior changed: Yes. ROWS eviction retains the next unread input row for unbounded-start frames.
- Does this need documentation: No
@Mryange

Mryange commented Aug 28, 2026

Copy link
Copy Markdown
Contributor 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 summary for current head 9a7fd0cafda68df7fc73f0e9763287c46887a52d

The prior blocking concern is resolved at aggregate HEAD: the eviction path now classifies both streaming ROWS executors, retains the bounded sliding frame's outgoing row, and retains the unbounded-start executor's next unread row. I found no additional blocking issue in the changed code.

Critical checkpoints

  • Goal and proof: The strict cutoff is correct for half-open prefix erase. Sliding incremental functions need frame_start - 1; unbounded-start cumulative evaluation needs current + rows_end_offset. The partition-start maximum covers clipped/empty early frames, and positive-FOLLOWING prefill remains protected by the older output-block gate. The new tests select both executors and assert the expected results across the BE_TEST reclamation boundary.
  • Scope and clarity: The change is focused to executor classification, the constant-time retention gate, nonnegative physical-coordinate rebasing, and targeted BE unit coverage. The three-file scope is appropriate.
  • Concurrency and lifecycle: The changed fields and coordinates are sink-local. Existing source-buffer and EOS locking are unchanged, and no new thread, lock-order, static-initialization, ownership, or teardown issue was found.
  • Parallel paths and conditions: PRECEDING, CURRENT ROW, FOLLOWING, incremental, reset/recompute, nullable, min/max rescan, partition transition, RANGE, whole-partition, and forced non-streaming paths were traced. Saturation collapses only an erased prefix; live ends and queued boundaries remain valid after rebasing.
  • Errors, memory, performance, and observability: No Status/exception path or allocation ownership changes. The gate adds O(1) work and retains only the rows required by the active frame/next unread input until the block-aligned cutoff becomes safe. Existing removal counters/timer and memory high-water mark remain sufficient.
  • Configuration, compatibility, persistence, and writes: No configuration, FE-BE variable, function symbol, wire/storage format, EditLog, transaction, data-write, cloud-mode, or rolling-upgrade behavior is changed.
  • Tests and standards: The widened sliding sums and new unbounded-start sums are correct, and BoundaryPose saturation is covered. No build, unit test, or regression test was run by this automated review, as required by the review prompt. The pure-text BE build-hygiene gate passed.
  • Candidate disposition: A Round 1 reviewer reproduced stale physical-row identity in pointer/offset-retaining first_value/nth_value state. Base comparison proves the same output-only eviction and in-place erase predate this PR, aggregate HEAD can only defer that eviction, and the live prior review already records it as a separate follow-up. It was therefore dismissed with evidence rather than duplicated inline.

User focus: No additional user-provided focus was supplied.

Review status: Complete after two rounds. All Round 2 normal and risk-focused reviewers returned NO_NEW_VALUABLE_FINDINGS; every candidate is accepted, deduplicated, or dismissed with evidence. No new inline comments are proposed.

@Mryange

Mryange commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17575	3131	3083	3083
q2	2155	267	241	241
q3	10169	827	531	531
q4	4675	248	204	204
q5	7679	573	386	386
q6	135	113	99	99
q7	533	485	386	386
q8	9241	910	896	896
q9	3453	2375	2383	2375
q10	6513	870	726	726
q11	398	205	179	179
q12	620	259	204	204
q13	18118	1528	1151	1151
q14	159	149	138	138
q15	q16	430	398	370	370
q17	1386	924	788	788
q18	3044	2277	2240	2240
q19	1098	833	756	756
q20	358	283	203	203
q21	5010	1746	1898	1746
q22	334	275	228	228
Total cold run time: 93083 ms
Total hot run time: 16930 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3534	3456	3447	3447
q2	494	401	368	368
q3	2198	2337	2188	2188
q4	1213	1170	917	917
q5	2200	2133	2119	2119
q6	174	118	89	89
q7	1028	945	879	879
q8	1601	1436	1441	1436
q9	3166	3148	3147	3147
q10	1843	1790	1640	1640
q11	364	275	256	256
q12	459	426	345	345
q13	1495	1530	1163	1163
q14	175	181	168	168
q15	q16	399	399	359	359
q17	3610	3374	3272	3272
q18	4894	4453	4786	4453
q19	852	933	907	907
q20	1007	977	809	809
q21	3897	3250	3191	3191
q22	395	344	329	329
Total cold run time: 34998 ms
Total hot run time: 31482 ms

@hello-stephen

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

query5	4265	411	338	338
query6	383	134	125	125
query7	4955	402	230	230
query8	289	119	118	118
query9	8701	2889	2854	2854
query10	418	228	176	176
query11	5381	1028	920	920
query12	115	70	68	68
query13	1203	445	315	315
query14	6147	2199	2086	2086
query14_1	1989	1981	1958	1958
query15	184	117	113	113
query16	949	375	356	356
query17	802	464	380	380
query18	2341	327	241	241
query19	171	152	114	114
query20	73	70	69	69
query21	207	103	86	86
query22	5347	5285	5239	5239
query23	6789	6334	6118	6118
query23_1	6132	6057	6103	6057
query24	7300	1092	770	770
query24_1	771	778	766	766
query25	430	305	260	260
query26	1236	243	128	128
query27	2782	422	258	258
query28	4658	1507	1504	1504
query29	919	444	368	368
query30	255	157	133	133
query31	859	410	337	337
query32	132	84	73	73
query33	500	223	196	196
query34	1026	836	497	497
query35	411	412	349	349
query36	588	572	526	526
query37	127	85	78	78
query38	1019	841	838	838
query39	531	481	457	457
query39_1	467	457	479	457
query40	216	94	80	80
query41	60	57	57	57
query42	76	73	74	73
query43	245	252	211	211
query44	1023	544	551	544
query45	107	110	109	109
query46	799	836	528	528
query47	747	764	746	746
query48	320	305	215	215
query49	538	247	177	177
query50	729	259	195	195
query51	8215	8229	8555	8229
query52	68	65	58	58
query53	194	192	150	150
query54	223	175	152	152
query55	77	84	84	84
query56	187	160	154	154
query57	695	673	658	658
query58	214	161	148	148
query59	1240	1235	1117	1117
query60	232	181	184	181
query61	116	115	123	115
query62	353	200	185	185
query63	164	142	138	138
query64	2866	693	667	667
query65	1572	1633	1601	1601
query66	1895	277	238	238
query67	9922	9621	9768	9621
query68	2791	1182	736	736
query69	349	231	200	200
query70	669	618	618	618
query71	243	178	173	173
query72	2315	1743	1591	1591
query73	671	608	347	347
query74	1560	1230	1188	1188
query75	1172	1085	954	954
query76	2294	736	538	538
query77	244	259	217	217
query78	4142	3853	3278	3278
query79	2431	855	610	610
query80	1609	329	263	263
query81	491	163	139	139
query82	748	119	94	94
query83	279	212	189	189
query84	301	111	87	87
query85	797	354	299	299
query86	395	175	170	170
query87	1023	955	899	899
query88	2778	2108	2131	2108
query89	293	196	172	172
query90	1982	127	131	127
query91	134	122	97	97
query92	76	69	68	68
query93	1616	1087	701	701
query94	674	249	183	183
query95	524	265	226	226
query96	805	555	268	268
query97	1083	1053	1036	1036
query98	162	132	132	132
query99	428	346	306	306
Total cold run time: 178429 ms
Total hot run time: 82579 ms

@hello-stephen

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

query1	0.00	0.00	0.01
query2	0.09	0.04	0.04
query3	0.25	0.10	0.11
query4	1.59	0.10	0.10
query5	0.18	0.17	0.17
query6	1.22	0.71	0.68
query7	0.03	0.00	0.01
query8	0.04	0.04	0.03
query9	0.29	0.21	0.23
query10	0.36	0.34	0.36
query11	0.17	0.12	0.11
query12	0.14	0.12	0.12
query13	0.32	0.30	0.30
query14	0.46	0.44	0.45
query15	0.37	0.37	0.36
query16	0.23	0.24	0.23
query17	0.67	0.68	0.69
query18	0.19	0.18	0.17
query19	1.22	1.19	1.15
query20	0.01	0.01	0.01
query21	15.44	0.16	0.11
query22	5.04	0.04	0.04
query23	16.17	0.25	0.11
query24	3.04	0.32	0.26
query25	0.11	0.04	0.03
query26	0.71	0.16	0.12
query27	0.04	0.03	0.03
query28	3.61	0.58	0.31
query29	12.43	3.22	2.58
query30	0.25	0.12	0.12
query31	2.76	0.37	0.17
query32	3.52	0.33	0.23
query33	1.61	1.44	1.55
query34	15.36	2.27	1.80
query35	1.75	1.71	1.75
query36	0.47	0.31	0.28
query37	0.06	0.04	0.04
query38	0.05	0.03	0.03
query39	0.03	0.02	0.02
query40	0.11	0.08	0.07
query41	0.08	0.03	0.02
query42	0.03	0.03	0.02
query43	0.03	0.03	0.03
Total cold run time: 90.53 s
Total hot run time: 14.81 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 62.73% (29344/46781)
Line Coverage 47.70% (306942/643455)
Region Coverage 43.32% (247855/572152)
Branch Coverage 44.89% (115398/257045)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.14% (34499/45309)
Line Coverage 61.13% (388834/636092)
Region Coverage 57.24% (326253/569969)
Branch Coverage 58.07% (148851/256312)

@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

@HappenLee
HappenLee merged commit ad179b5 into apache:master Aug 31, 2026
33 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 31, 2026
Problem Summary: Streaming `ROWS` window aggregates retain state across
frame evaluations. The eviction path previously considered only whether
buffered blocks had been emitted, so it could erase either the outgoing
row needed by a bounded sliding frame or the next unread row needed by
an `UNBOUNDED PRECEDING ... N PRECEDING` frame. After rebasing, negative
partition and outgoing positions could allow a nullable aggregate to
access its null map out of bounds. Evicting either kind of required row
could also produce incorrect aggregate results.

Root cause: `_remove_unused_rows()` did not account for the earliest row
required by the next ROWS frame evaluation, and
`BoundaryPose::remove_unused_rows()` allowed retained-column coordinates
to become negative.

This change defers block-aligned eviction when the candidate prefix
contains either `frame_start - 1`, the outgoing row required by a
bounded sliding update, or the next unread row required by an `UNBOUNDED
PRECEDING ... N PRECEDING` frame. It also rebases partition and order
boundaries to nonnegative physical-column coordinates. The BE unit
coverage exercises both ROWS executors across eviction boundaries and
verifies boundary rebasing.

Observed ASAN failure before this change (`output/be/log/be.out`):

```text
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
    #0 doris::AggregateFunctionNullUnaryInlineV2<...>::execute_function_with_incremental(...)
       be/src/exprs/aggregate/aggregate_function_null_v2.h:595
    #1 doris::AggFnEvaluator::execute_function_with_incremental(...)
       be/src/exprs/vectorized_agg_fn.cpp:334
    #2 doris::AnalyticSinkLocalState::_execute_for_function<true>(...)
       be/src/exec/operator/analytic_sink_operator.cpp:385
    #3 doris::AnalyticSinkLocalState::_get_next_for_sliding_rows(...)
       be/src/exec/operator/analytic_sink_operator.cpp:203
    #4 doris::AnalyticSinkLocalState::_execute_impl(...)
       be/src/exec/operator/analytic_sink_operator.cpp:358
    #5 doris::AnalyticSinkOperatorX::sink_impl(...)
       be/src/exec/operator/analytic_sink_operator.cpp:757
SUMMARY: AddressSanitizer: heap-buffer-overflow in
doris::AggregateFunctionNullUnaryInlineV2<...>::execute_function_with_incremental(...)
```


### Release note

None

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [ ] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason <!-- Add your reason?  -->

- Behavior changed:
    - [ ] No.
    - [ ] Yes. <!-- Explain the behavior change -->

- Does this need documentation?
    - [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants