Skip to content

[opt](exec) Align operator projection with scanner (without copy when do project) - #66085

Open
Mryange wants to merge 2 commits into
apache:masterfrom
Mryange:project-not-copy
Open

[opt](exec) Align operator projection with scanner (without copy when do project)#66085
Mryange wants to merge 2 commits into
apache:masterfrom
Mryange:project-not-copy

Conversation

@Mryange

@Mryange Mryange commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Projection results were converted to mutable columns before being published to the output block. For shared columns such as SlotRef results, IColumn::mutate() triggered a COW clone and copied the entire column.

This change publishes immutable ColumnPtr results directly in both operator and scanner projection paths, while preserving the output schema and scanner origin-block lifecycle.

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

@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 Jul 27, 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.

Automated review summary — REQUEST_CHANGES

I found five distinct issues in the generic operator projection rewrite: supported nullable widening can now fail, runtime slot columns can be published under an incompatible retained DataType, projection allocations no longer inform the workload-group reservation gate, pass-through slot projections clone while discarding reusable output capacity, and an empty intermediate stage can silently drop nonempty rows in release builds.

Critical checkpoints:

  • Goal and proof: the intended no-copy alignment is not achieved for slot refs, and the PR includes no targeted ownership/reuse/reservation test or benchmark.
  • Correctness and error handling: two P1 type/nullability contract regressions and one P2 release-mode invariant regression are detailed inline.
  • Lifecycle and concurrency: there is no new shared-state or lock-order issue, but the COW owner lifecycle/output-block reuse is incorrect, and the pipeline memory-pressure scheduler is under-informed.
  • Compatibility and parallel paths: there are no new FE-BE fields, storage-format, configuration, persistence, or data-write changes. The scanner analogue does not establish that the generic operator contracts are safe, and malformed/compatibility thrift input still requires runtime rejection.
  • Performance and memory: slot-ref projections allocate and deep-copy on each reused block, while the new allocations are absent from the next-round reservation estimate.
  • Tests: no tests are added and the PR checklist is entirely unchecked. Formatting/title/static repository checks are green; BE UT was still pending at review time. Per the review-run contract, I did not run builds or tests locally.
  • Observability/security: no security-sensitive behavior is changed; the new generic Nullable mismatch exception is also less diagnosable than the removed contextual failures.
  • User focus: no additional focus was provided, so the whole PR was reviewed.

The _keep_origin field removal itself has no remaining callers and is safe, but do_projections() is not ready to merge until the inline issues are addressed with focused correctness, ownership/reuse, and reservation coverage. Two complete review rounds converged, with all three independent Round 2 reviewers returning NO_NEW_VALUABLE_FINDINGS for this frozen five-comment set.


size_t bytes_usage = 0;
ColumnsWithTypeAndName new_columns;
std::vector<int> result_column_ids;

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.

[P1] Keep projection allocations in the next-round reservation estimate

This rewrite removes every update to local_state->_estimate_memory_usage. OperatorX::get_reserve_mem_size() reads that value, and PipelineTask consumes/resets it before the next get_block_after_projects() to decide whether to pause/revoke/spill. Moving a result into output_block after expression evaluation does not remove the allocation: a large string/complex intermediate or final projection will now reserve only the minimum and can allocate through the workload-group pressure gate. Please keep an observed or sound pre-execution estimate for newly allocated projection data (without counting genuinely shared pass-through storage) and cover a repeated large variable-width projection with reservation enabled.

input_block.shuffle_columns(result_column_ids);
}

DCHECK_EQ(rows, input_block.rows());

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.

[P2] Keep a release-mode rejection for an empty intermediate stage

init() still accepts an empty element in intermediate_projections_list; resize(0) followed by shuffle_columns({}) creates a zero-column block whose rows() is zero. These replacement checks disappear in release builds, so final expressions execute with count zero and a nonempty input can return Status::OK() with no rows, whereas the deleted check returned InternalError. Please restore the empty-stage/runtime row-count invariant (the per-expression size checks can rely on execute_column()) and cover the release behavior with a malformed-plan/unit test.

Comment thread be/src/exec/operator/operator.cpp Outdated
insert_column_datas(mutable_columns[i], column_ptr, rows);
DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << debug_string();

for (int i = 0; i < mutable_columns.size(); ++i) {

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.

[P2] Avoid cloning pass-through slots while discarding the reusable buffer

For a final slot ref, origin_block, the shallow input_block, and column_ptr all own the same column when IColumn::mutate() runs, so shallow_mutate() takes its deep-clone path. Meanwhile mutable_columns[i] is the retained-capacity output column cleared by PipelineTask, and this assignment destroys it. A slot-only projection therefore performs a fresh allocation plus full-column copy on every block. Please publish the immutable result without requiring mutable ownership, or release the source owners before a real transfer, and add a reused-block slot projection benchmark/test.

Comment thread be/src/exec/operator/operator.cpp Outdated
ColumnPtr column_ptr;
RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, column_ptr));
column_ptr = column_ptr->convert_to_full_column_if_const();
if (mutable_columns[i]->is_nullable() != column_ptr->is_nullable()) {

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.

[P1] Preserve the supported nullable widening here

VExpr::check_expr_output_type() explicitly accepts T -> Nullable(T), and VExpr::execute_column() also allows a declared nullable expression to return a physically non-nullable column when the evaluated rows contain no nulls. Such a plan passes prepare/expression execution but now throws here; the deleted branch attached an all-zero null map. Please preserve this one-way mismatch with make_nullable(..., false) (or equivalent) before transferring ownership, while still rejecting Nullable(T) -> T, and add a focused positive-row test.

Comment thread be/src/exec/operator/operator.cpp Outdated
throw Exception(ErrorCode::INTERNAL_ERROR, "Nullable mismatch");
}
DCHECK(mutable_block.rows() == rows);
mutable_columns[i] = IColumn::mutate(std::move(column_ptr));

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.

[P1] Validate the result against the destination type before replacing its pointer

This block keeps the DataType created from _output_row_descriptor, but VSlotRef::execute_type(block) intentionally validates a returned slot column against the runtime input type, which can differ from the expression's declared/output type. With equal nullability (for example declared/output INT but runtime BIGINT), the check above passes and this installs ColumnInt64 under DataTypeInt32. The old typed insert_range_from failed loudly through release-enabled assert_cast; please validate against the destination type or materialize into the typed destination before replacing it.

@Mryange

Mryange commented Jul 27, 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.

Automated follow-up review — REQUEST_CHANGES

The current head fixes the pass-through COW clone reported in discussion r3655092239: the generic operator and scanner paths now publish immutable ColumnPtr results directly, and the reviewed ownership/free-block lifecycle is safe. No new inline finding survived validation and deduplication, so I am not repeating existing comments.

Four previously filed blockers remain present on this head:

  • [P1] Supported non-null T results still throw when the destination is Nullable(T) (r3655092246).
  • [P1] A runtime SlotRef column can still be published under an incompatible retained destination DataType (r3655092252).
  • [P1] Computed/intermediate projection allocations still no longer inform the next-round reservation gate (r3655092228).
  • [P2] An empty intermediate stage can still silently drop nonempty rows in release builds (r3655092233).

Critical checkpoints:

  • Goal and proof: immutable publication now achieves the intended no-forced-clone handoff, but the remaining correctness, reservation, and release-invariant defects mean the PR is not ready to merge. No focused test or benchmark was added.
  • Scope and parallel paths: the three-file change is focused, both generic and scanner publication paths were reviewed, and _keep_origin was already dead. The scanner lifecycle change itself is safe; its analogous nullable/intermediate behavior predates this PR.
  • Concurrency and lifecycle: projection contexts and blocks are task/scanner-local, scanner results cross the existing locked task/context handoff, and COW-aware clearing preserves shared output data. No new lock-order, deadlock, static-lifetime, or cross-thread mutation issue was found.
  • Error handling and compatibility: expression Status values remain checked, but the two existing type-contract failures and release-only empty-stage behavior remain. There is no new protocol, storage-format, configuration, rolling-upgrade, persistence, transaction, data-write, or FE-BE variable change.
  • Performance and memory: the old shared-SlotRef deep copy is resolved, while the existing reservation-accounting blocker remains for allocating projections. No separate changed-line scanner allocation defect was substantiated.
  • Tests and observability: existing tests exercise ordinary projections but do not prove immutable identity, nullable widening, destination runtime-type validation, malformed empty stages, reservation-enabled variable-width repetition, or scanner free-block reuse. Per the runner contract, no local build or test was run.
  • User focus: no additional focus was provided, so the whole PR was reviewed.

Two current-head review rounds, including full operator/scanner coverage and a separate risk-focused pass, converged on this frozen zero-inline set with every candidate either deduplicated or dismissed with concrete evidence.

@Mryange

Mryange commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

/review

• The remaining comments describe hypothetical or malformed-plan cases rather than regressions from this PR:

  1. Final projection types are validated against the output descriptor during prepare. FE inserts casts for required type
    conversions, and invalid conversions fail through the cast path. Please provide a valid FE-generated plan that can
    publish an incompatible physical column or requires nullable widening at this boundary.

  2. The removed allocated_bytes() accounting was not a sound reservation estimate: it counted shared SlotRef storage,
    reusable capacity, and intermediate columns repeatedly. Actual allocations remain tracked by MemTracker. Redesigning
    reservation estimation should be handled separately rather than restoring incorrect accounting.

  3. Empty intermediate stages cannot be generated by FE. The only producer is the Nereids multi-layer CSE translator,
    where every intermediate layer contains input slots and/or extracted aliases. An empty stage requires malformed thrift
    and is outside the supported plan contract.

The current head resolves the actual COW clone by publishing immutable projection columns directly in both operator and
scanner paths. Please re-review the current implementation.

@Mryange

Mryange commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17766	3917	3901	3901
q2	2018	352	204	204
q3	10251	1465	817	817
q4	4682	467	338	338
q5	7529	842	568	568
q6	194	173	137	137
q7	737	797	594	594
q8	9353	1536	1615	1536
q9	5461	4184	4148	4148
q10	6793	1674	1440	1440
q11	509	372	322	322
q12	717	575	455	455
q13	18094	3429	2752	2752
q14	272	261	246	246
q15	q16	763	749	677	677
q17	939	1076	1001	1001
q18	7005	5744	5502	5502
q19	1302	1213	1085	1085
q20	758	719	590	590
q21	5966	2659	2301	2301
q22	432	357	296	296
Total cold run time: 101541 ms
Total hot run time: 28910 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4224	4123	4231	4123
q2	297	315	207	207
q3	4533	4881	4349	4349
q4	2035	2118	1327	1327
q5	4294	4166	4158	4158
q6	228	175	127	127
q7	1707	1591	1414	1414
q8	2212	1939	1879	1879
q9	6852	6865	6850	6850
q10	4334	4373	3943	3943
q11	530	383	351	351
q12	712	724	493	493
q13	3020	3448	2769	2769
q14	278	293	255	255
q15	q16	672	690	605	605
q17	1256	1212	1223	1212
q18	7304	6827	6722	6722
q19	1126	1103	1123	1103
q20	2213	2184	1932	1932
q21	5214	4528	4378	4378
q22	525	454	414	414
Total cold run time: 53566 ms
Total hot run time: 48611 ms

@hello-stephen

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

query5	2701	610	471	471
query6	433	221	201	201
query7	4843	590	344	344
query8	339	185	162	162
query9	8769	4069	4065	4065
query10	473	371	316	316
query11	5907	2248	2001	2001
query12	166	108	98	98
query13	1258	619	433	433
query14	6655	4746	4496	4496
query14_1	3856	3869	3832	3832
query15	214	214	177	177
query16	1078	497	441	441
query17	1286	699	595	595
query18	2650	472	355	355
query19	303	200	156	156
query20	114	107	112	107
query21	235	163	136	136
query22	12930	12934	12754	12754
query23	17605	16413	16028	16028
query23_1	16080	16227	16115	16115
query24	7706	1698	1239	1239
query24_1	1270	1249	1254	1249
query25	569	447	383	383
query26	1310	377	220	220
query27	2435	600	359	359
query28	4364	2009	1994	1994
query29	1093	626	497	497
query30	428	260	222	222
query31	1120	1074	958	958
query32	117	68	62	62
query33	569	304	253	253
query34	1161	1122	647	647
query35	745	761	631	631
query36	983	991	909	909
query37	156	107	92	92
query38	1823	1704	1611	1611
query39	880	863	843	843
query39_1	814	827	822	822
query40	248	167	143	143
query41	64	62	65	62
query42	93	92	94	92
query43	318	319	273	273
query44	1454	770	755	755
query45	187	174	171	171
query46	1087	1189	710	710
query47	1784	1792	1715	1715
query48	410	424	305	305
query49	731	434	299	299
query50	1132	438	351	351
query51	11020	11061	10905	10905
query52	92	86	79	79
query53	268	269	207	207
query54	291	229	218	218
query55	74	71	67	67
query56	298	283	292	283
query57	1159	1152	1049	1049
query58	260	257	242	242
query59	1544	1602	1477	1477
query60	272	273	250	250
query61	152	151	157	151
query62	448	387	326	326
query63	231	199	206	199
query64	1806	1051	852	852
query65	3938	3875	3844	3844
query66	1652	481	370	370
query67	28253	28347	28147	28147
query68	3214	1587	906	906
query69	385	286	266	266
query70	1041	917	918	917
query71	389	341	298	298
query72	2996	2660	2283	2283
query73	833	816	429	429
query74	4622	4481	4311	4311
query75	2411	2395	2046	2046
query76	2061	1155	799	799
query77	402	381	275	275
query78	11362	11178	10696	10696
query79	1371	1127	785	785
query80	890	555	464	464
query81	502	325	290	290
query82	599	156	120	120
query83	376	322	332	322
query84	277	160	131	131
query85	941	625	531	531
query86	428	274	259	259
query87	1789	1795	1718	1718
query88	3726	2798	2760	2760
query89	415	340	301	301
query90	2042	199	194	194
query91	204	195	168	168
query92	62	61	56	56
query93	1596	1486	941	941
query94	710	339	325	325
query95	791	600	470	470
query96	1048	812	339	339
query97	2477	2458	2370	2370
query98	199	197	189	189
query99	735	735	616	616
Total cold run time: 252894 ms
Total hot run time: 171315 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.04
query3	0.26	0.13	0.14
query4	1.61	0.14	0.14
query5	0.25	0.24	0.22
query6	1.22	1.08	1.05
query7	0.04	0.01	0.01
query8	0.06	0.05	0.04
query9	0.38	0.33	0.30
query10	0.54	0.54	0.55
query11	0.20	0.14	0.14
query12	0.18	0.14	0.14
query13	0.46	0.47	0.47
query14	0.99	0.99	0.99
query15	0.60	0.59	0.58
query16	0.31	0.32	0.35
query17	1.13	1.06	1.10
query18	0.21	0.20	0.20
query19	2.08	1.91	1.91
query20	0.01	0.01	0.01
query21	15.46	0.19	0.13
query22	4.94	0.05	0.06
query23	16.11	0.31	0.13
query24	2.95	0.44	0.34
query25	0.10	0.06	0.04
query26	0.75	0.20	0.14
query27	0.05	0.03	0.04
query28	3.51	0.78	0.34
query29	12.51	4.24	3.43
query30	0.28	0.15	0.16
query31	2.78	0.57	0.31
query32	3.22	0.59	0.47
query33	3.19	3.16	3.24
query34	15.44	3.96	3.27
query35	3.23	3.21	3.20
query36	0.56	0.44	0.41
query37	0.10	0.06	0.07
query38	0.06	0.03	0.04
query39	0.04	0.03	0.04
query40	0.17	0.15	0.15
query41	0.08	0.03	0.03
query42	0.04	0.02	0.02
query43	0.04	0.04	0.04
Total cold run time: 96.24 s
Total hot run time: 24.25 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 81.48% (22/27) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 58.13% (24636/42383)
Line Coverage 42.19% (246144/583485)
Region Coverage 38.07% (195541/513593)
Branch Coverage 39.20% (88274/225181)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 96.30% (26/27) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.25% (31102/41330)
Line Coverage 59.80% (346631/579655)
Region Coverage 56.54% (291419/515384)
Branch Coverage 57.89% (130388/225249)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants