Skip to content

[fix](nereids) Unify null-input safety checks for join pushdown - #67889

Open
morrySnow wants to merge 1 commit into
apache:masterfrom
morrySnow:fix/guard-null-side-project-pushdown
Open

[fix](nereids) Unify null-input safety checks for join pushdown#67889
morrySnow wants to merge 1 commit into
apache:masterfrom
morrySnow:fix/guard-null-side-project-pushdown

Conversation

@morrySnow

@morrySnow morrySnow commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Problem

Expressions evaluated around outer joins can observe NULL-extended rows. Several optimizer components independently approximated whether such expressions remain NULL: project pushdown had no nullable-side guard, runtime-filter pushdown used a shallow Slot/Cast/PropagateNullable check, and eager aggregation relied on a global marker that both over-blocked safe nested expressions and could not reason about each candidate side. These inconsistent checks could cause incorrect project/filter placement or unnecessarily reject safe runtime-filter and aggregate pushdown.

Root cause

The optimizer lacked one fail-closed, typed SQL-NULL evaluation primitive. The existing general constant-fold entry point may invoke BE folding when enabled, which is unsuitable for physical post-processing and repeated optimizer safety checks. Runtime filters therefore duplicated structural logic, while eager aggregation scanned for null-to-non-null markers instead of evaluating the complete aggregate argument after project substitution and for the actual null-generating side.

Reproduction

  • Above a left outer join, element_at(coalesce(right_array, [99]), 1) must return 99 for an unmatched row. The old project pushdown could evaluate it below the join and return NULL instead.
  • A runtime-filter probe c_custkey + cast(c_custkey is null as int) evaluates to NULL when the nullable-side key is NULL, but the old structural classifier could not prove it and stopped safe pushdown; coalesce(c_custkey, 0) must remain blocked.
  • max(t2.id2 + cast(t2.id2 is null as int)) remains NULL on a NULL-extended right side and is safe to pre-aggregate, but the old marker scan rejected it merely because it contained IS NULL.

Fix

  • Keep the shared five-state typed-NULL evaluator (NULL, FALSE, TRUE, OTHER_NON_NULL, UNKNOWN) and its fail-closed consumers in not-null inference, hypergraph conflict rules, and project/filter pushdown.
  • Add an FE-only FoldConstantRule entry point with identical debug-skip and exception behavior that never invokes BE-folding RPC, and expose it through NullInputEvaluator without duplicating replacement or classification.
  • Thread ExpressionRewriteContext through every runtime-filter pushdown factory, including CTE and min/max paths, preserve it when rewriting probe expressions, and allow traversal of a non-builder outer join's nullable child only when the complete typed-NULL probe evaluates exactly to SQL NULL.
  • Replace eager aggregation's global marker with a per-candidate-side, per-aggregate-argument proof after project substitution. count(*), non-NULL literals, incomplete folds, and decomposed aggregate IF remain blocked; ordinary nullable columns, NULL literals, and complete nested expressions that evaluate to NULL remain eligible.
  • Remove the now-unused null-to-non-null marker interface and implementations.

Tests

  • 88 focused FE unit tests passed across NullInputEvaluatorTest, PushDownProjectTest, EagerAggRewriterTest, RuntimeFilterTest, and ExpressionUtilsTest. They cover five-state classification, debug-skip fail-closed behavior, FE-only evaluation with BE folding enabled, safe and unsafe runtime-filter expressions and the concrete customer-scan target, count/literal/project-hidden eager-aggregation hazards, and the formerly over-blocked nested expression.
  • Full FE build passed across 80 modules with zero Checkstyle violations using DISABLE_BUILD_UI=ON ./build.sh --fe.
  • Sandbox regressions passed for outer-join project null extension, outer-join runtime-filter safety, and eager aggregation. The eager suite required its standard load suite first because the sandbox initially lacked the TPC-DS fixture tables.
  • git diff --check completed with no output.

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

@morrySnow

Copy link
Copy Markdown
Contributor Author

run buildall

@morrySnow morrySnow changed the title [fix](nereids) Guard project pushdown across null extension [fix](rbo) Guard project pushdown across null extension Sep 12, 2026
@morrySnow
morrySnow marked this pull request as draft September 12, 2026 03:29
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: Expressions evaluated around outer joins can observe NULL-extended rows, but project pushdown, runtime-filter pushdown, and eager aggregation used different approximations of whether an expression remains NULL. Project and filter expressions could be moved below null extension and return an incorrect value, while shallow structural checks and global marker scans could also reject safe runtime filters and aggregate pushdown. Introduce one fail-closed typed-NULL evaluator, add an FE-only folding path for optimizer safety checks, and reuse the exact complete-expression result across logical and physical pushdown decisions. Runtime-filter contexts now carry the rewrite context through all creation and probe-rewrite paths, eager aggregation proves every argument for each candidate nullable side, and the obsolete null-to-non-null marker interface is removed.

### Release note

Fix incorrect results from pushing expressions that turn outer-join NULL inputs into non-NULL values below null extension, and use precise NULL-input proofs for safe runtime-filter and eager-aggregation pushdown.

### Check List (For Author)

- Test: Unit tests, regression tests, and full FE build
    - Five related FE unit-test classes passed 88 tests, including typed-NULL classification, project/filter guards, runtime-filter targets, eager-aggregation safety, and existing expression utilities
    - Sandbox regressions passed for outer-join project null extension, outer-join runtime-filter safety, and eager aggregation after loading its standard fixture data
    - Full FE build passed across 80 modules with zero Checkstyle violations using DISABLE_BUILD_UI=ON
- Behavior changed: Yes. Unsafe pushdown across null-generating join sides is rejected, while complete expressions proven to evaluate to SQL NULL remain eligible.
- Does this need documentation: No
@morrySnow
morrySnow force-pushed the fix/guard-null-side-project-pushdown branch from ca02b36 to f966529 Compare September 12, 2026 10:01
@morrySnow morrySnow changed the title [fix](rbo) Guard project pushdown across null extension [fix](nereids) Unify null-input safety checks for join pushdown Sep 12, 2026
@morrySnow

Copy link
Copy Markdown
Contributor Author

run buildall

@morrySnow
morrySnow marked this pull request as ready for review September 12, 2026 10:02
@morrySnow
morrySnow requested a review from yujun777 as a code owner September 12, 2026 10:02
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17601	3045	3038	3038
q2	2088	259	223	223
q3	10235	899	520	520
q4	4673	245	204	204
q5	7680	583	388	388
q6	139	116	96	96
q7	536	562	406	406
q8	9235	989	935	935
q9	3463	2437	2427	2427
q10	6510	874	727	727
q11	399	196	183	183
q12	616	268	202	202
q13	18135	1532	1173	1173
q14	159	157	143	143
q15	q16	431	397	373	373
q17	1316	915	795	795
q18	3142	2282	2284	2282
q19	1257	944	827	827
q20	382	288	198	198
q21	5621	1608	1915	1608
q22	333	273	235	235
Total cold run time: 93951 ms
Total hot run time: 16983 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3417	3362	3321	3321
q2	506	398	393	393
q3	2213	2401	2239	2239
q4	1207	1178	905	905
q5	2190	2155	2136	2136
q6	163	121	87	87
q7	1060	914	829	829
q8	1599	1389	1391	1389
q9	3177	3174	3140	3140
q10	1896	1806	1627	1627
q11	364	274	256	256
q12	462	437	350	350
q13	1482	1538	1178	1178
q14	168	183	159	159
q15	q16	397	392	359	359
q17	3629	3305	3368	3305
q18	4903	4484	4910	4484
q19	957	864	876	864
q20	1042	1018	823	823
q21	3882	3185	3237	3185
q22	408	352	327	327
Total cold run time: 35122 ms
Total hot run time: 31356 ms

@hello-stephen

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

query5	4262	421	347	347
query6	380	135	134	134
query7	4920	414	237	237
query8	289	124	120	120
query9	8685	2910	2928	2910
query10	414	225	192	192
query11	5399	1070	934	934
query12	115	73	73	73
query13	1193	457	312	312
query14	6119	2236	2108	2108
query14_1	1996	1985	1995	1985
query15	182	124	112	112
query16	929	389	320	320
query17	787	469	374	374
query18	2338	324	238	238
query19	168	139	113	113
query20	77	71	70	70
query21	204	102	89	89
query22	5561	5389	5300	5300
query23	6936	6331	5947	5947
query23_1	6107	6284	6240	6240
query24	7255	1081	758	758
query24_1	777	806	775	775
query25	430	299	256	256
query26	1231	235	129	129
query27	2785	405	252	252
query28	4716	1514	1496	1496
query29	930	441	353	353
query30	239	156	133	133
query31	832	407	339	339
query32	132	75	82	75
query33	464	220	177	177
query34	989	828	495	495
query35	405	413	355	355
query36	580	563	526	526
query37	122	80	73	73
query38	1018	867	830	830
query39	525	514	513	513
query39_1	516	462	458	458
query40	198	92	80	80
query41	60	55	59	55
query42	77	73	73	73
query43	239	243	214	214
query44	991	539	545	539
query45	115	111	102	102
query46	843	869	520	520
query47	778	764	722	722
query48	328	302	236	236
query49	547	254	201	201
query50	785	255	201	201
query51	8515	8267	8349	8267
query52	70	100	71	71
query53	191	198	140	140
query54	197	167	164	164
query55	71	59	63	59
query56	181	159	168	159
query57	679	696	667	667
query58	213	166	159	159
query59	1235	1232	1110	1110
query60	228	191	168	168
query61	111	111	140	111
query62	354	192	189	189
query63	169	139	151	139
query64	2667	718	597	597
query65	1608	1587	1651	1587
query66	1773	252	193	193
query67	10125	9744	9784	9744
query68	3033	1249	703	703
query69	346	229	199	199
query70	646	615	620	615
query71	247	174	161	161
query72	2288	1702	1525	1525
query73	626	571	331	331
query74	1996	1231	1174	1174
query75	1192	1112	968	968
query76	2378	705	515	515
query77	262	250	211	211
query78	3954	3717	3253	3253
query79	2387	864	590	590
query80	1611	322	260	260
query81	487	154	136	136
query82	608	130	96	96
query83	277	207	189	189
query84	297	109	90	90
query85	774	338	285	285
query86	390	194	174	174
query87	1033	972	900	900
query88	2781	2093	2117	2093
query89	284	200	174	174
query90	2003	127	136	127
query91	129	118	101	101
query92	80	70	71	70
query93	1447	1075	713	713
query94	620	269	232	232
query95	517	241	298	241
query96	758	582	287	287
query97	1058	1071	1028	1028
query98	172	146	131	131
query99	419	351	315	315
Total cold run time: 178869 ms
Total hot run time: 82676 ms

@hello-stephen

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

query1	0.01	0.01	0.01
query2	0.08	0.04	0.04
query3	0.25	0.12	0.10
query4	1.61	0.10	0.09
query5	0.17	0.16	0.17
query6	1.26	0.68	0.72
query7	0.03	0.01	0.00
query8	0.04	0.04	0.03
query9	0.27	0.22	0.22
query10	0.34	0.36	0.37
query11	0.17	0.13	0.12
query12	0.15	0.12	0.12
query13	0.31	0.31	0.33
query14	0.46	0.45	0.45
query15	0.37	0.35	0.36
query16	0.21	0.22	0.24
query17	0.73	0.74	0.72
query18	0.19	0.17	0.18
query19	1.20	1.16	1.19
query20	0.02	0.01	0.02
query21	15.44	0.15	0.12
query22	5.03	0.04	0.04
query23	16.18	0.26	0.10
query24	3.06	0.32	0.26
query25	0.12	0.04	0.04
query26	0.77	0.16	0.13
query27	0.04	0.03	0.02
query28	3.67	0.54	0.28
query29	12.47	3.24	2.59
query30	0.26	0.10	0.12
query31	2.75	0.38	0.17
query32	3.56	0.31	0.25
query33	1.40	1.45	1.56
query34	15.41	2.15	1.81
query35	1.78	1.77	1.74
query36	0.46	0.29	0.30
query37	0.06	0.04	0.04
query38	0.04	0.04	0.03
query39	0.03	0.02	0.02
query40	0.11	0.08	0.08
query41	0.08	0.03	0.02
query42	0.04	0.02	0.02
query43	0.03	0.03	0.03
Total cold run time: 90.66 s
Total hot run time: 14.95 s

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