Skip to content

[fix](connector) rebuild connector scan properties after column pruning - #66369

Open
morningman wants to merge 2 commits into
apache:masterfrom
morningman:test-jdbc-column-pruning-coverage
Open

[fix](connector) rebuild connector scan properties after column pruning#66369
morningman wants to merge 2 commits into
apache:masterfrom
morningman:test-jdbc-column-pruning-coverage

Conversation

@morningman

@morningman morningman commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Related PR: #64304 (catalog SPI)

Problem Summary:

A connector never decides which columns to read — it renders whatever list ConnectorScanRequest.getColumns() carries. The jdbc connector turns that list verbatim into the remote SELECT list and falls back to SELECT * when it is empty (JdbcQueryBuilder#buildQuery). So column pruning for every plugin-driven external scan rests entirely on PluginDrivenScanNode#buildColumnHandles(), which intersects the connector's column handles with this scan's tuple slots.

That method had no direct coverage, and its failure mode (projecting more columns than the query needs) is a pure performance regression that no result-comparing test can observe. The existing jdbc explain assertions all pass a column list and assert those same columns are present; none of them can fail on an over-wide projection they did not anticipate.

This PR started as coverage for that gap. The new coverage immediately found a real bug, so it now carries the fix as well.

1. The bug: the connector's scan properties are computed before column pruning

A plugin-driven scan asks its connector for one property bundle — the jdbc remote SELECT, per-column dictionaries, file format, path partition keys — and caches it (cachedPropertiesResult / scanNodeProperties).

That cache is first filled from init():

PhysicalPlanTranslator#getPlanFragmentForPhysicalFileScan
  -> scanNode.init()
     -> FileQueryScanNode#doInitialize -> initSchemaParams -> getPathPartitionKeys()
        -> PluginDrivenScanNode#getPathPartitionKeys -> getOrLoadScanNodeProperties()

init() runs while the translator is still translating this scan — strictly before the project above it prunes the tuple down to the columns the query reads (updateScanSlotsMaterialization). Everything the connector derives from the projection at that point therefore describes the full table schema.

The only thing that dropped the cache was convertPredicate(), and only when there was a conjunct to push down. Queries with a WHERE clause were rebuilt from the pruned tuple by accident; filter-less ones kept the pre-pruning bundle. Result:

-- doris_test.test1 has 12 columns
explain select count(*) from test1;
-- before: QUERY: SELECT `k1`, `k2`, ..., `k12` FROM `doris_test`.`test1`
-- after:  QUERY: SELECT `k1` FROM `doris_test`.`test1`

This holds for every WHERE-less query on any plugin-driven external table, not just count(*).

The scan itself was not affected — getSplits() rebuilds the column handles from the final tuple, so the query actually sent to the source was already pruned. What was wrong is the reported remote query, and anything else a connector derives from the projection through this bundle (populateScanLevelParams, getFileAttributes). Reviewers of the iceberg connector may want to check the field-id dictionary applied in IcebergScanPlanProvider#populateScanLevelParams, which is built from the requested columns and, on a filter-less query, was built over the full schema.

Fix: drop the cache in doFinalize(), the first point at which the tuple is final. Every filtered query already exercises this rebuild path today — including the second MVCC-snapshot / rewrite-scope pin it implies — so the filter-less path is only being moved onto an already-exercised path.

Why it survived since the SPI migration: all 13 remote-query assertions in the tree filter, and a filter is exactly what used to hide this. The one filter-less assertion that exists (test_gbase_jdbc_catalog, commented out) expects the pruned single column, i.e. the behavior this restores.

2. The projection decision itself (fe-core)

PluginDrivenScanNodeColumnPruningTest drives the real buildColumnHandles() and pins:

  • only tuple-slot columns are projected (3-column table, 1 requested → exactly 1 handle);
  • the order follows the slot order, not the connector's handle-map order — the connector renders this list positionally;
  • slots with no backing column, and slots with no matching handle, are skipped rather than leaking into the list;
  • an empty tuple projects nothing — the sole input that reaches the connector's SELECT * fallback.

Every assertion was mutation-checked against the production method: returning allHandles.values() kills 4 of the 5, and making the unmatched-slot path fail loud unconditionally kills the 5th.

3. Explain assertions (external_table_p0)

Two additions to test_mysql_jdbc_catalog, both of which fail without the fix above:

  • a filter-less projection (select k8 from test1) — the shape no existing assertion covered, and the most direct pin for the caching bug;
  • count(*), the one shape whose projection would otherwise go empty. The engine keeps a single smallest slot (PhysicalPlanTranslator#updateScanSlotsMaterialization) instead of letting the tuple go empty, and an empty tuple is exactly what makes the jdbc connector emit SELECT *. The assertion pins that the remote select list stays one column wide and is not *; it deliberately does not pin which column wins, since that is getSmallestSlot's business and tracks type widths.

Release note

Fix EXPLAIN reporting a full-width remote read for queries without a WHERE clause on external catalogs backed by the connector plugin framework (e.g. jdbc). The scan itself already read only the projected columns.

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:
  • Behavior changed:

    • No.
    • Yes. EXPLAIN on a plugin-driven external scan without a WHERE clause now reports the pruned column list instead of the full schema. Query results and the SQL actually sent to the source are unchanged.
  • 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

A connector never decides which columns to read: it renders whatever list
`ConnectorScanRequest.getColumns()` carries. The jdbc connector turns that
list verbatim into the remote SELECT list and falls back to `SELECT *` when
it is empty, so the whole of column pruning for every plugin-driven external
scan rests on `PluginDrivenScanNode.buildColumnHandles()` intersecting the
connector's column handles with the scan's tuple slots.

That method had no direct coverage, and the failure mode it guards against --
projecting more columns than the query needs -- is a pure performance
regression that no result-comparing test can observe. This adds:

- PluginDrivenScanNodeColumnPruningTest: drives the real projection and pins
  that only tuple-slot columns are projected, that the order follows the slot
  order (the connector renders the list positionally), that column-less and
  unmatched slots are skipped, and that an empty tuple projects nothing --
  the sole input that reaches the connector's `SELECT *` fallback.

- an explain assertion in test_mysql_jdbc_catalog for `count(*)`, the one
  query shape whose projection would otherwise go empty. The engine keeps a
  single smallest slot instead, so the remote SQL must stay one column wide
  rather than degrading to a 12-column `SELECT *` just to count rows. The
  assertion pins the arity of the select list, not which column wins, since
  that is `getSmallestSlot`'s business and tracks type widths.

Test-only change; no production code is touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/850) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17673	3990	3976	3976
q2	2064	332	209	209
q3	10231	1447	825	825
q4	4680	470	340	340
q5	7506	851	553	553
q6	188	178	140	140
q7	766	803	604	604
q8	9343	1632	1632	1632
q9	5269	4127	4082	4082
q10	6746	1650	1372	1372
q11	510	372	349	349
q12	738	571	448	448
q13	18091	3357	2719	2719
q14	263	259	243	243
q15	q16	735	734	664	664
q17	1042	923	998	923
q18	6867	5687	5452	5452
q19	1337	1313	972	972
q20	778	706	587	587
q21	5958	2667	2333	2333
q22	436	352	299	299
Total cold run time: 101221 ms
Total hot run time: 28722 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4273	4183	4209	4183
q2	286	316	208	208
q3	4560	4934	4395	4395
q4	2195	2309	1420	1420
q5	4222	4185	4103	4103
q6	225	169	126	126
q7	1679	1598	1431	1431
q8	3043	2131	2188	2131
q9	7526	7589	7560	7560
q10	4285	4290	3877	3877
q11	600	398	370	370
q12	731	733	509	509
q13	3206	3530	2895	2895
q14	302	292	293	292
q15	q16	721	711	660	660
q17	1309	1301	1273	1273
q18	8035	7335	7233	7233
q19	1172	1100	1116	1100
q20	2232	2227	1943	1943
q21	5275	4595	4478	4478
q22	512	448	394	394
Total cold run time: 56389 ms
Total hot run time: 50581 ms

@hello-stephen

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

query5	4304	621	466	466
query6	479	242	227	227
query7	4846	574	358	358
query8	331	186	180	180
query9	8857	4058	4066	4058
query10	472	352	327	327
query11	5842	2224	1998	1998
query12	153	100	97	97
query13	1250	593	433	433
query14	6095	4704	4405	4405
query14_1	3789	3781	3763	3763
query15	218	211	179	179
query16	1007	469	441	441
query17	895	681	549	549
query18	2411	468	339	339
query19	232	177	141	141
query20	103	105	100	100
query21	227	155	136	136
query22	13101	12998	12906	12906
query23	17360	16382	16015	16015
query23_1	16142	16086	16191	16086
query24	7509	1721	1225	1225
query24_1	1244	1230	1242	1230
query25	540	432	350	350
query26	1356	363	215	215
query27	2582	630	375	375
query28	4544	2051	2055	2051
query29	1063	589	472	472
query30	343	267	237	237
query31	1150	1080	958	958
query32	113	62	65	62
query33	547	324	256	256
query34	1195	1132	670	670
query35	732	759	633	633
query36	792	792	685	685
query37	158	108	103	103
query38	1854	1666	1640	1640
query39	844	812	800	800
query39_1	797	786	789	786
query40	260	173	146	146
query41	72	85	89	85
query42	99	95	96	95
query43	319	323	278	278
query44	1427	789	783	783
query45	194	183	170	170
query46	1096	1155	729	729
query47	1561	1509	1433	1433
query48	419	424	302	302
query49	587	422	316	316
query50	1062	447	346	346
query51	10630	10279	10474	10279
query52	94	89	75	75
query53	258	286	207	207
query54	291	247	228	228
query55	76	73	67	67
query56	295	314	317	314
query57	1050	1002	930	930
query58	308	268	254	254
query59	1558	1616	1464	1464
query60	298	285	270	270
query61	179	166	167	166
query62	403	328	276	276
query63	247	199	198	198
query64	2977	1133	992	992
query65	3942	3838	3823	3823
query66	1828	470	354	354
query67	28265	28288	28079	28079
query68	3115	1527	978	978
query69	420	295	263	263
query70	908	824	781	781
query71	372	350	327	327
query72	3043	2661	2431	2431
query73	845	773	454	454
query74	4641	4485	4331	4331
query75	2389	2362	1978	1978
query76	2336	1141	756	756
query77	341	367	268	268
query78	11340	11125	10634	10634
query79	1374	1150	798	798
query80	1251	534	456	456
query81	581	326	288	288
query82	623	157	114	114
query83	377	319	298	298
query84	333	161	137	137
query85	960	622	565	565
query86	415	231	231	231
query87	1784	1793	1691	1691
query88	3770	2820	2839	2820
query89	409	322	284	284
query90	1963	207	189	189
query91	206	192	163	163
query92	64	59	56	56
query93	1742	1535	968	968
query94	733	363	325	325
query95	785	599	464	464
query96	1051	802	327	327
query97	2503	2476	2314	2314
query98	205	204	191	191
query99	717	729	617	617
Total cold run time: 257035 ms
Total hot run time: 169798 ms

@hello-stephen

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

query1	0.01	0.01	0.01
query2	0.10	0.04	0.04
query3	0.26	0.14	0.13
query4	1.61	0.15	0.14
query5	0.24	0.21	0.22
query6	1.16	0.81	0.80
query7	0.03	0.01	0.00
query8	0.06	0.04	0.04
query9	0.38	0.30	0.31
query10	0.58	0.54	0.56
query11	0.20	0.14	0.14
query12	0.19	0.14	0.14
query13	0.46	0.46	0.47
query14	1.00	0.98	1.00
query15	0.60	0.59	0.58
query16	0.31	0.33	0.31
query17	1.08	1.07	1.10
query18	0.22	0.22	0.22
query19	2.03	1.90	1.88
query20	0.02	0.02	0.02
query21	15.45	0.23	0.14
query22	4.73	0.06	0.05
query23	16.19	0.31	0.11
query24	2.94	0.42	0.34
query25	0.12	0.05	0.04
query26	0.74	0.22	0.14
query27	0.04	0.04	0.03
query28	3.54	0.83	0.36
query29	12.58	4.09	3.24
query30	0.27	0.16	0.15
query31	2.78	0.55	0.32
query32	3.23	0.59	0.49
query33	3.16	3.18	3.25
query34	15.44	3.96	3.29
query35	3.27	3.25	3.21
query36	0.56	0.43	0.45
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.18	0.15	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 96.11 s
Total hot run time: 23.89 s

A plugin-driven scan asks its connector for one property bundle -- the jdbc
remote SELECT, per-column dictionaries, file format, path partition keys --
and caches it. That cache is first filled from `init()`
(`FileQueryScanNode.initSchemaParams` -> `getPathPartitionKeys`), which runs
while `PhysicalPlanTranslator` is still translating this scan, i.e. strictly
before the project above it prunes the tuple down to the columns the query
actually reads. Everything the connector derives from the projection at that
point therefore describes the FULL table schema.

Only `convertPredicate()` dropped that cache, and only when there was a
conjunct to push down. Queries with a WHERE clause were rebuilt from the pruned
tuple by accident; filter-less ones kept the pre-pruning bundle. So
`explain select count(*) from <jdbc table>` reported a 12-column remote read of
a 12-column table, and every WHERE-less query on any plugin-driven external
table reported a full-width read. The scan itself was unaffected -- `getSplits`
rebuilds the column handles from the final tuple -- but the reported remote
query was wrong, and so is anything else a connector derives from the
projection through this bundle (`populateScanLevelParams`, `getFileAttributes`).

Drop the cache in `doFinalize()` instead: that is the first point at which the
tuple is final. Every filtered query already exercises this rebuild path today,
including the second MVCC-snapshot / rewrite-scope pin it implies, so the
filter-less path is only being moved onto it.

Also adds the assertion the suite never had: a remote-query check on a query
with no WHERE clause. Every existing one filters, which is exactly why this
survived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@morningman morningman changed the title [test](jdbc) cover connector column pruning at the projection layer [fix](connector) rebuild connector scan properties after column pruning Aug 3, 2026
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17594	3860	3843	3843
q2	1998	316	199	199
q3	10305	1342	807	807
q4	4692	469	340	340
q5	7501	826	568	568
q6	200	165	135	135
q7	748	800	588	588
q8	10207	1559	1508	1508
q9	5921	4027	4045	4027
q10	6802	1612	1359	1359
q11	515	351	317	317
q12	740	567	451	451
q13	18066	3257	2688	2688
q14	267	254	244	244
q15	q16	726	727	658	658
q17	1786	1091	900	900
q18	6684	5619	5480	5480
q19	1427	1321	1093	1093
q20	775	663	544	544
q21	5625	2559	2299	2299
q22	428	356	298	298
Total cold run time: 103007 ms
Total hot run time: 28346 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4248	4153	4181	4153
q2	290	314	211	211
q3	4530	4923	4432	4432
q4	2141	2238	1389	1389
q5	4176	4127	4093	4093
q6	221	171	129	129
q7	1662	1549	1385	1385
q8	2770	2097	2078	2078
q9	7199	7311	7210	7210
q10	4310	4300	3840	3840
q11	541	400	371	371
q12	708	710	515	515
q13	3151	3692	2792	2792
q14	293	308	287	287
q15	q16	696	717	685	685
q17	1267	1306	1296	1296
q18	7991	7127	7230	7127
q19	1097	1040	1072	1040
q20	2168	2177	1891	1891
q21	5176	4567	4347	4347
q22	536	454	404	404
Total cold run time: 55171 ms
Total hot run time: 49675 ms

@hello-stephen

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

query5	4329	618	483	483
query6	459	222	207	207
query7	4871	585	344	344
query8	329	185	162	162
query9	8765	4003	4018	4003
query10	464	349	302	302
query11	5837	2168	2049	2049
query12	150	93	94	93
query13	1281	582	432	432
query14	6081	4677	4319	4319
query14_1	3783	3753	3754	3753
query15	213	195	170	170
query16	984	466	434	434
query17	914	674	533	533
query18	2415	455	334	334
query19	221	199	148	148
query20	106	110	112	110
query21	243	156	138	138
query22	13056	12996	12754	12754
query23	17142	16335	15959	15959
query23_1	16066	16025	16055	16025
query24	7538	1692	1234	1234
query24_1	1234	1215	1215	1215
query25	513	401	358	358
query26	1324	341	224	224
query27	2619	647	376	376
query28	4472	1995	1977	1977
query29	1037	582	456	456
query30	332	261	227	227
query31	1116	1080	964	964
query32	102	68	59	59
query33	521	305	237	237
query34	1181	1092	638	638
query35	721	751	624	624
query36	780	761	695	695
query37	150	113	92	92
query38	1824	1626	1584	1584
query39	827	806	794	794
query39_1	810	804	815	804
query40	239	158	137	137
query41	65	64	65	64
query42	91	90	88	88
query43	310	314	270	270
query44	1379	777	755	755
query45	184	186	165	165
query46	1084	1174	768	768
query47	1598	1582	1514	1514
query48	427	442	331	331
query49	583	414	297	297
query50	1121	440	349	349
query51	10396	9946	10407	9946
query52	87	84	77	77
query53	262	262	194	194
query54	280	236	219	219
query55	73	70	64	64
query56	289	288	284	284
query57	1009	1015	938	938
query58	283	235	251	235
query59	1544	1527	1397	1397
query60	302	259	251	251
query61	141	144	145	144
query62	398	321	271	271
query63	228	197	189	189
query64	3437	990	951	951
query65	3863	3805	3790	3790
query66	1816	480	369	369
query67	28162	28089	27935	27935
query68	3492	1473	1016	1016
query69	403	314	275	275
query70	915	814	790	790
query71	380	342	320	320
query72	3112	2813	2449	2449
query73	841	812	442	442
query74	4639	4494	4297	4297
query75	2428	2351	1996	1996
query76	2323	1139	781	781
query77	337	365	264	264
query78	11169	11048	10479	10479
query79	1416	1118	761	761
query80	1304	542	470	470
query81	522	332	282	282
query82	584	150	113	113
query83	367	314	298	298
query84	342	160	132	132
query85	962	594	501	501
query86	399	237	225	225
query87	1795	1796	1697	1697
query88	3770	2857	2809	2809
query89	394	322	280	280
query90	1871	192	194	192
query91	201	191	159	159
query92	59	57	54	54
query93	1700	1467	980	980
query94	728	360	320	320
query95	786	590	466	466
query96	1046	858	341	341
query97	2452	2456	2319	2319
query98	200	210	193	193
query99	713	730	615	615
Total cold run time: 256134 ms
Total hot run time: 168190 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.05
query3	0.25	0.14	0.14
query4	1.60	0.13	0.14
query5	0.23	0.21	0.22
query6	1.16	0.83	0.79
query7	0.04	0.01	0.01
query8	0.05	0.04	0.03
query9	0.37	0.30	0.29
query10	0.54	0.56	0.53
query11	0.19	0.14	0.13
query12	0.18	0.14	0.13
query13	0.46	0.47	0.45
query14	0.99	1.01	1.01
query15	0.59	0.58	0.57
query16	0.32	0.33	0.32
query17	1.09	1.14	1.10
query18	0.21	0.20	0.19
query19	1.97	1.95	1.92
query20	0.02	0.01	0.01
query21	15.43	0.22	0.14
query22	4.74	0.05	0.05
query23	16.13	0.33	0.12
query24	2.96	0.44	0.32
query25	0.11	0.05	0.05
query26	0.73	0.22	0.14
query27	0.05	0.03	0.03
query28	3.52	0.79	0.34
query29	12.49	4.03	3.21
query30	0.28	0.16	0.15
query31	2.77	0.56	0.31
query32	3.22	0.58	0.48
query33	3.23	3.20	3.23
query34	15.55	3.94	3.30
query35	3.27	3.24	3.21
query36	0.55	0.44	0.42
query37	0.09	0.06	0.07
query38	0.05	0.04	0.04
query39	0.04	0.02	0.02
query40	0.17	0.15	0.15
query41	0.08	0.04	0.03
query42	0.04	0.04	0.03
query43	0.04	0.04	0.03
Total cold run time: 95.9 s
Total hot run time: 23.84 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 0.00% (0/4) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (4/4) 🎉
Increment coverage report
Complete coverage report

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