Skip to content

[fix](scan) Read all nested access paths before evaluating TopN filters on pruned STRUCT columns - #68110

Merged
yiguolei merged 1 commit into
apache:masterfrom
mrhhsg:fix/topn-filter-nested-column-pruning
Sep 20, 2026
Merged

yiguolei merged 1 commit into
apache:masterfrom
mrhhsg:fix/topn-filter-nested-column-pruning

Conversation

@mrhhsg

@mrhhsg mrhhsg commented Sep 17, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Problem Summary:

With enable_prune_nested_column = true, the planner splits a STRUCT column's
access paths into predicate paths (read before filtering) and lazy paths (read
only for surviving rows). SegmentIterator relies on this split when a common
expression references the column: it reads the column in the PREDICATE phase,
evaluates the expression, and recovers the lazy nested fields afterwards.

The TopN filter is a common expression that BE attaches to the scan at runtime,
after the planner computed the access paths. When ORDER BY and WHERE reference
different fields of the same STRUCT, for example

SELECT struct_element(s, 'a') FROM t
WHERE struct_element(s, 'b') IS NOT NULL
ORDER BY 1 NULLS LAST LIMIT 1;

the predicate paths only contain s.b.NULL, so s.a is a placeholder (NULL)
while the TopN filter struct_element(s, 'a') <= current_top is evaluated.
Every row of the later tablets is rejected and the global minimum is lost,
silently returning a wrong LIMIT result.

This PR marks columns referenced by runtime-generated common expressions
(TopN filters and runtime filters) and keeps them in the NORMAL read phase, so
all access paths are materialized before the expression is evaluated. The lazy
split is still used for planner-visible expressions.

Release note

None

Check List (For Author)

  • Test:
    • Unit Test: SegmentIteratorRuntimeCommonExprTest covers the lazy split
      decision for planner expressions, TopN filters, and both on one column.
    • Regression test: topn_filter_nested_column_pruning reproduces the wrong
      result with TIMESTAMPTZ/INT/DATE STRUCT fields, ASC/DESC, NULLS FIRST/LAST,
      selective predicates and multi-tablet scans.
  • Behavior changed: No
  • Does this need documentation: No

https://claude.ai/code/session_01E3gDAafAXZELs6AHVfD9UG

…rs on pruned STRUCT columns

### What problem does this PR solve?

Issue Number: None

Problem Summary:

With `enable_prune_nested_column = true`, the planner splits a STRUCT column's
access paths into predicate paths (read before filtering) and lazy paths (read
only for surviving rows). SegmentIterator relies on this split when a common
expression references the column: it reads the column in the PREDICATE phase,
evaluates the expression, and recovers the lazy nested fields afterwards.

The TopN filter is a common expression that BE attaches to the scan at runtime,
after the planner computed the access paths. When ORDER BY and WHERE reference
different fields of the same STRUCT, for example

```sql
SELECT struct_element(s, 'a') FROM t
WHERE struct_element(s, 'b') IS NOT NULL
ORDER BY 1 NULLS LAST LIMIT 1;
```

the predicate paths only contain `s.b.NULL`, so `s.a` is a placeholder (NULL)
while the TopN filter `struct_element(s, 'a') <= current_top` is evaluated.
Every row of the later tablets is rejected and the global minimum is lost,
silently returning a wrong LIMIT result.

This PR marks columns referenced by runtime-generated common expressions
(TopN filters and runtime filters) and keeps them in the NORMAL read phase, so
all access paths are materialized before the expression is evaluated. The lazy
split is still used for planner-visible expressions.

### Release note

None

### Check List (For Author)

- Test:
    - Unit Test: `SegmentIteratorRuntimeCommonExprTest` covers the lazy split
      decision for planner expressions, TopN filters, and both on one column.
    - Regression test: `topn_filter_nested_column_pruning` reproduces the wrong
      result with TIMESTAMPTZ/INT/DATE STRUCT fields, ASC/DESC, NULLS FIRST/LAST,
      selective predicates and multi-tablet scans.
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01E3gDAafAXZELs6AHVfD9UG
@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?

@mrhhsg

mrhhsg commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review outcome: no blocking findings. The change is focused and the runtime-generated TopN/runtime-filter path is handled correctly.

Critical checkpoint conclusions:

  • Goal and correctness: the new per-column state distinguishes BE-runtime-generated common expressions from planner-visible common expressions. For STRUCT/ARRAY/MAP columns used by those expressions, suppressing lazy nested recovery makes both initial and row-id common-expression reads use NORMAL phase, so every FE-retained predicate and lazy-output descendant is materialized before the runtime expression evaluates. TopN and runtime-filter roots retain their identity through normalization/cloning, and virtual slots are expanded with the runtime flag preserved.
  • Scope and clarity: the production change is confined to SegmentIterator state marking and lazy-materialization planning. The conservative eager read applies only to retained complex columns referenced by a runtime-generated common expression.
  • Concurrency: no new shared mutable state or locking is introduced; the added flags are iterator-local initialization state. I traced adaptive scanner admission, nonempty-block/EOS resubmission, TopN bound publication, and the p0 one-BE topology. The regression serial variants deterministically publish a bucket-1 bound before scanning bucket 0, which contains the global extrema and exercises the old failure.
  • Lifecycle and resources: the state is rebuilt and initialized with the existing SegmentIterator lifecycle, is monotonically ORed across expressions, and adds no ownership, cleanup, static-initialization, or memory-accounting concern.
  • Configuration and compatibility: no configuration, protocol, persisted/storage format, public symbol, or rolling-upgrade contract changes are introduced.
  • Parallel and special paths: TopN and runtime-filter wrappers, predicate/common overlap, mixed planner/runtime expressions, index-consumed expressions, virtual slots, meta paths, and STRUCT/ARRAY/MAP read-phase behavior were checked. No parallel path requiring a corresponding production change was found.
  • Tests and expected results: the BE unit rejects the old lazy-planning decision and covers planner-only, TopN-only, and mixed-expression state. The ordered p0 regression covers different-field ASC/DESC cases across TIMESTAMPTZ, INT, and DATE, plus selective, same-field, multi-STRUCT, and default-parallel shapes; its expected output matches the inserted data. The LIMIT-2 and NULLS-FIRST labels provide extra coverage but are not relied on to prove the repaired branch. Test discovery was verified statically.
  • Observability, transactions, and cross-layer variables: no additional observability is needed for this local read-planning decision. Transaction/persistence/data-write concerns and new FE-to-BE variables are not applicable.
  • Performance: runtime-generated filters may read all FE-retained nested paths for the affected complex column instead of recovering lazy children later. That bounded extra I/O is the necessary correctness fallback because BE-generated predicate paths are unavailable to FE pruning.
  • User focus: no additional review focus was provided.

Review status: complete for the supplied head, but static-only. Per the review environment constraints I did not build Doris or execute the BE/regression tests. git diff --check and the static build-hygiene gate passed; visible GitHub formatting, license, title, and secret/large-file checks were also green at review time.

@mrhhsg

mrhhsg commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17720	4037	3975	3975
q2	2264	353	297	297
q3	10080	1397	817	817
q4	4685	476	347	347
q5	7581	828	548	548
q6	184	174	142	142
q7	730	761	586	586
q8	9343	1470	1535	1470
q9	5446	4194	4225	4194
q10	6725	1637	1360	1360
q11	454	269	249	249
q12	633	410	302	302
q13	18022	2595	2002	2002
q14	257	252	232	232
q15	q16	734	714	665	665
q17	1761	1148	992	992
q18	6475	5654	5556	5556
q19	1287	1296	973	973
q20	480	387	262	262
q21	5870	3036	2615	2615
q22	455	371	315	315
Total cold run time: 101186 ms
Total hot run time: 27899 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4794	4948	4693	4693
q2	805	599	586	586
q3	4874	5225	4702	4702
q4	2250	2301	1474	1474
q5	4562	4376	4628	4376
q6	249	179	138	138
q7	1791	1685	1459	1459
q8	2306	2004	2041	2004
q9	7453	7253	7238	7238
q10	4267	4250	3859	3859
q11	528	367	347	347
q12	708	725	507	507
q13	2343	2644	2001	2001
q14	267	272	248	248
q15	q16	668	683	602	602
q17	7279	6807	6654	6654
q18	11882	11053	11775	11053
q19	1110	1000	1016	1000
q20	2207	2211	1914	1914
q21	5427	4529	4616	4529
q22	504	453	421	421
Total cold run time: 66274 ms
Total hot run time: 59805 ms

@hello-stephen

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

query5	4310	606	457	457
query6	426	196	161	161
query7	4889	524	300	300
query8	314	173	166	166
query9	8794	3999	3969	3969
query10	447	305	242	242
query11	5825	2183	2002	2002
query12	146	99	94	94
query13	1271	546	412	412
query14	6535	4560	4254	4254
query14_1	4042	3995	4022	3995
query15	210	196	178	178
query16	988	491	459	459
query17	905	700	562	562
query18	2428	454	344	344
query19	198	176	146	146
query20	109	105	108	105
query21	223	135	115	115
query22	12992	13126	12809	12809
query23	15554	14662	14046	14046
query23_1	14129	14191	14103	14103
query24	7534	1704	1214	1214
query24_1	1266	1249	1263	1249
query25	527	437	365	365
query26	1257	304	162	162
query27	2715	541	344	344
query28	4585	1973	1948	1948
query29	1061	643	444	444
query30	331	233	203	203
query31	901	767	632	632
query32	133	94	93	93
query33	517	303	232	232
query34	1198	1149	625	625
query35	720	751	664	664
query36	831	791	698	698
query37	150	104	85	85
query38	1829	1786	1707	1707
query39	681	692	658	658
query39_1	676	669	641	641
query40	221	118	102	102
query41	70	67	64	64
query42	91	95	89	89
query43	333	346	292	292
query44	1352	690	693	690
query45	196	176	170	170
query46	1047	1148	712	712
query47	1488	1533	1395	1395
query48	396	363	286	286
query49	572	406	294	294
query50	934	337	248	248
query51	10346	10029	10241	10029
query52	84	85	73	73
query53	251	259	179	179
query54	244	224	197	197
query55	77	72	69	69
query56	229	222	223	222
query57	1478	1367	1346	1346
query58	238	209	208	208
query59	1945	2048	1808	1808
query60	261	248	228	228
query61	150	148	146	146
query62	398	316	266	266
query63	217	174	171	171
query64	2827	1013	821	821
query65	3996	4016	3921	3921
query66	1794	431	301	301
query67	20036	20050	19945	19945
query68	3297	1460	806	806
query69	415	295	263	263
query70	965	880	850	850
query71	306	230	205	205
query72	2969	2444	2182	2182
query73	809	809	436	436
query74	4629	4497	4256	4256
query75	2297	2287	1927	1927
query76	2302	1078	709	709
query77	353	398	310	310
query78	9043	9065	8413	8413
query79	1233	1101	712	712
query80	512	458	362	362
query81	451	279	237	237
query82	248	161	129	129
query83	268	272	239	239
query84	289	141	109	109
query85	783	464	390	390
query86	277	242	232	232
query87	1976	1959	1835	1835
query88	3634	2750	2722	2722
query89	327	284	246	246
query90	2106	183	177	177
query91	165	150	132	132
query92	101	91	90	90
query93	1380	1380	944	944
query94	530	357	307	307
query95	665	363	427	363
query96	1063	795	337	337
query97	2461	2507	2308	2308
query98	202	187	193	187
query99	742	726	611	611
Total cold run time: 239870 ms
Total hot run time: 153307 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.25	0.13	0.13
query4	1.60	0.13	0.13
query5	0.25	0.22	0.22
query6	1.16	0.98	0.91
query7	0.04	0.01	0.00
query8	0.05	0.04	0.03
query9	0.39	0.34	0.33
query10	0.55	0.55	0.55
query11	0.21	0.14	0.14
query12	0.18	0.15	0.15
query13	0.47	0.47	0.47
query14	0.95	0.93	0.94
query15	0.63	0.59	0.59
query16	0.29	0.31	0.31
query17	1.08	1.05	1.06
query18	0.22	0.20	0.20
query19	2.06	1.90	1.95
query20	0.02	0.01	0.01
query21	15.43	0.21	0.14
query22	4.76	0.05	0.06
query23	16.13	0.31	0.13
query24	3.01	0.47	0.34
query25	0.10	0.05	0.04
query26	0.73	0.21	0.14
query27	0.05	0.04	0.03
query28	3.50	0.80	0.37
query29	12.48	3.98	3.21
query30	0.28	0.16	0.15
query31	2.77	0.57	0.31
query32	3.22	0.59	0.50
query33	3.10	3.26	3.16
query34	15.52	3.94	3.28
query35	3.24	3.24	3.22
query36	0.56	0.43	0.42
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.04	0.04	0.03
query40	0.18	0.15	0.14
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.03	0.03
Total cold run time: 95.91 s
Total hot run time: 23.94 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 63.52% (29576/46562)
Line Coverage 48.26% (308101/638483)
Region Coverage 43.84% (248589/566985)
Branch Coverage 45.41% (115598/254556)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.28% (34395/45090)
Line Coverage 61.17% (386134/631211)
Region Coverage 57.56% (325032/564712)
Branch Coverage 58.38% (148161/253767)

@mrhhsg
mrhhsg marked this pull request as ready for review September 18, 2026 02:16
@yiguolei
yiguolei merged commit 9521f75 into apache:master Sep 20, 2026
46 checks passed
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.

6 participants