Skip to content

[fix](fk) Isolate primary key proofs by relation instance - #67891

Open
morrySnow wants to merge 1 commit into
apache:masterfrom
morrySnow:fix/isolate-foreign-key-proof-instances
Open

[fix](fk) Isolate primary key proofs by relation instance#67891
morrySnow wants to merge 1 commit into
apache:masterfrom
morrySnow:fix/isolate-foreign-key-proof-instances

Conversation

@morrySnow

Copy link
Copy Markdown
Contributor

Problem

Foreign-key join elimination could produce incorrect results for a self-referencing table when LIMIT or TopN invalidated the primary-key side. The optimizer could remove the join and return rows that had no match in the limited primary input.

Root cause

ForeignKeyContext represented both catalog-declared primary keys and the validity of a particular relation instance with the same set of qualified catalog columns. When a limited scan expired its proof, visiting another scan of the same physical table registered the catalog primary key again. Because both scans shared the same catalog identity, this revived the expired proof for the first scan even though their output slots had different ExprIds.

Reproduction

Create a self-referencing table containing (id, parent_id) = (1, 1), (2, 2), declare id as its primary key and parent_id as a foreign key to id, then join the table to (select id from the same table order by id limit 1). The valid join returns only 1. Before this change, join elimination removed the limited primary side and incorrectly returned 1, 2.

Fix

  • Keep catalog primary-key declarations in declaredPrimaryKeys.
  • Track usable primary-key proofs by exact output Slot/ExprId in activePrimaryKeySlots.
  • Activate a slot only when its catalog column is a declared primary key.
  • Expire only the slots output by the invalidating plan node.
  • Propagate an alias proof only when its origin slot is still active.
  • Keep existing foreign-key and predicate-compatibility semantics unchanged.

Tests

  • ./run-fe-ut.sh --run org.apache.doris.nereids.rules.rewrite.EliminateJoinByFkTest — 17 tests, 0 failures, 0 errors.
  • DISABLE_BUILD_UI=ON ./build.sh --fe — full FE reactor build passed; FE checkstyle reported 0 violations.
  • Generated the new regression output with -forceGenOut — 1 suite passed.
  • Ran the new regression normally against the rebuilt sandbox — 1 suite passed, 0 failed/fatal/skipped.
  • Manually verified that limited, reversed, aliased-over-TopN, and incompatible-filter plans retain the join, while plain and safely aliased plans remain eliminable.

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: Join elimination could return extra rows for a self-referencing foreign key when the primary-key relation was limited. A query joining (select id from self_ref order by id limit 1) back to another scan of self_ref should keep the join and return one row, but the optimizer eliminated the join and returned both foreign-side rows. ForeignKeyContext stored primary-key validity by catalog column identity, so expiring the limited relation removed a shared catalog key that a later scan of the same table registered again. This accidentally revived the invalid proof for the earlier relation instance. Track declared primary-key metadata separately from active Slot/ExprId proofs, expire only the exact plan output slots, and propagate activity through aliases only when the origin slot remains active.

### Release note

Fix incorrect results when a self-referencing foreign-key join has LIMIT or TopN on the primary-key relation.

### Check List (For Author)

- Test: Unit Test, Regression test, and Manual test
    - Added optimizer tests for plain, limited, aliased, projected, filtered, reversed, and unrelated relation cases.
    - Added a regression test that verifies the limited join is retained and returns one row while the unrestricted join remains eliminable.
    - Ran the focused FE unit test, full FE build with checkstyle, and the regression suite on a local FE/BE deployment.
- Behavior changed: Yes. Unsafe join elimination is disabled when LIMIT or TopN invalidates a relation-instance primary-key proof or when primary/foreign predicates are not compatible; proven safe elimination remains enabled.
- 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?

@morrySnow

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 90.91% (10/11) 🎉
Increment coverage report
Complete coverage report

@morrySnow morrySnow changed the title [fix](nereids) Isolate primary key proofs by relation instance [fix](fk) Isolate primary key proofs by relation instance Sep 12, 2026
@morrySnow
morrySnow marked this pull request as draft September 12, 2026 03:30
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17602	2979	2999	2979
q2	2059	262	228	228
q3	10250	874	509	509
q4	4670	250	201	201
q5	7676	562	385	385
q6	142	113	93	93
q7	526	486	388	388
q8	9237	927	901	901
q9	3450	2387	2372	2372
q10	6496	853	704	704
q11	391	198	181	181
q12	632	255	204	204
q13	18124	1519	1154	1154
q14	162	151	140	140
q15	q16	436	392	364	364
q17	1366	882	811	811
q18	3077	2258	2226	2226
q19	1252	911	763	763
q20	406	281	199	199
q21	5613	1686	1822	1686
q22	326	267	236	236
Total cold run time: 93893 ms
Total hot run time: 16724 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3340	3285	3278	3278
q2	496	389	363	363
q3	2189	2262	2166	2166
q4	1181	1164	871	871
q5	2144	2093	2092	2092
q6	165	114	87	87
q7	1068	909	884	884
q8	1564	1360	1360	1360
q9	3080	3070	3049	3049
q10	1857	1765	1665	1665
q11	344	268	248	248
q12	469	425	349	349
q13	1466	1525	1170	1170
q14	167	157	155	155
q15	q16	388	389	364	364
q17	3533	3231	3198	3198
q18	4774	4404	4710	4404
q19	836	830	819	819
q20	1133	977	832	832
q21	3831	3121	3314	3121
q22	393	350	334	334
Total cold run time: 34418 ms
Total hot run time: 30809 ms

@hello-stephen

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

query5	4238	407	351	351
query6	383	142	124	124
query7	4955	431	221	221
query8	281	122	120	120
query9	8690	2846	2896	2846
query10	383	227	185	185
query11	5380	1042	903	903
query12	121	71	70	70
query13	1199	406	332	332
query14	6096	2213	2079	2079
query14_1	1960	1963	1944	1944
query15	175	124	116	116
query16	927	362	354	354
query17	816	460	354	354
query18	2326	324	242	242
query19	176	136	109	109
query20	73	71	68	68
query21	198	100	90	90
query22	5363	5373	5336	5336
query23	6896	6237	6133	6133
query23_1	6069	5969	6230	5969
query24	7283	1070	760	760
query24_1	742	747	772	747
query25	404	284	227	227
query26	1215	216	126	126
query27	2812	433	248	248
query28	4654	1475	1480	1475
query29	908	427	333	333
query30	252	153	126	126
query31	823	389	320	320
query32	125	76	70	70
query33	474	203	169	169
query34	988	805	477	477
query35	390	395	345	345
query36	563	559	511	511
query37	118	75	67	67
query38	993	845	816	816
query39	477	504	502	502
query39_1	458	465	443	443
query40	205	89	80	80
query41	53	51	53	51
query42	73	72	74	72
query43	235	241	211	211
query44	991	535	538	535
query45	110	101	98	98
query46	746	809	522	522
query47	750	750	703	703
query48	310	293	230	230
query49	539	243	188	188
query50	748	277	195	195
query51	8134	7851	8003	7851
query52	71	72	60	60
query53	189	198	145	145
query54	218	178	153	153
query55	82	61	57	57
query56	195	165	164	164
query57	696	675	637	637
query58	201	237	173	173
query59	1212	1242	1130	1130
query60	260	199	172	172
query61	109	112	105	105
query62	380	204	182	182
query63	168	150	135	135
query64	2809	694	572	572
query65	1660	1652	1624	1624
query66	1809	288	197	197
query67	9739	9694	9597	9597
query68	2734	1201	771	771
query69	341	213	196	196
query70	679	621	612	612
query71	294	176	165	165
query72	2263	1649	1500	1500
query73	644	576	323	323
query74	1570	1225	1140	1140
query75	1169	1089	949	949
query76	2273	729	512	512
query77	246	261	197	197
query78	3911	3688	3237	3237
query79	1164	834	586	586
query80	1373	318	281	281
query81	489	154	134	134
query82	649	123	94	94
query83	286	213	196	196
query84	301	116	90	90
query85	1033	331	275	275
query86	377	176	168	168
query87	1019	982	918	918
query88	2775	2089	2097	2089
query89	286	203	184	184
query90	1872	134	122	122
query91	142	131	111	111
query92	84	66	75	66
query93	1375	1064	721	721
query94	646	274	245	245
query95	525	272	311	272
query96	801	597	268	268
query97	1077	1051	1014	1014
query98	144	136	141	136
query99	426	346	315	315
Total cold run time: 175489 ms
Total hot run time: 81595 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.08	0.04	0.04
query3	0.25	0.12	0.10
query4	1.60	0.10	0.09
query5	0.17	0.17	0.15
query6	1.28	0.68	0.68
query7	0.03	0.01	0.00
query8	0.05	0.03	0.03
query9	0.28	0.22	0.22
query10	0.36	0.37	0.35
query11	0.16	0.11	0.12
query12	0.14	0.12	0.11
query13	0.30	0.30	0.30
query14	0.45	0.45	0.46
query15	0.36	0.34	0.36
query16	0.22	0.23	0.23
query17	0.65	0.68	0.72
query18	0.18	0.18	0.17
query19	1.27	1.15	1.17
query20	0.02	0.01	0.01
query21	15.44	0.16	0.12
query22	5.11	0.04	0.04
query23	16.18	0.26	0.11
query24	2.97	0.30	0.25
query25	0.11	0.05	0.03
query26	0.78	0.16	0.11
query27	0.04	0.03	0.03
query28	3.66	0.55	0.28
query29	12.43	3.12	2.54
query30	0.25	0.10	0.12
query31	2.75	0.36	0.18
query32	3.53	0.30	0.23
query33	1.36	1.40	1.39
query34	15.38	2.24	1.78
query35	1.76	1.73	1.71
query36	0.45	0.29	0.29
query37	0.06	0.04	0.04
query38	0.05	0.04	0.03
query39	0.02	0.02	0.03
query40	0.11	0.08	0.08
query41	0.08	0.03	0.02
query42	0.03	0.02	0.02
query43	0.03	0.02	0.02
Total cold run time: 90.44 s
Total hot run time: 14.63 s

@morrySnow
morrySnow marked this pull request as ready for review September 12, 2026 07:55
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