Skip to content

[improvement](be) Compact selected row indices with mask lookups - #69818

Merged
HappenLee merged 2 commits into
apache:masterfrom
HappenLee:improvement/simd-selection-lookup
Oct 30, 2026
Merged

HappenLee merged 2 commits into
apache:masterfrom
HappenLee:improvement/simd-selection-lookup

Conversation

@HappenLee

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Problem Summary:

SegmentIterator expands partially selected SIMD masks one set bit at a time, creating a serial dependency for every selected row. Expand x86 masks with more than two selected rows using a constexpr eight-row lookup table and fixed-width stores. Keep the existing bit iteration for sparse masks and other architectures. Empty/full masks, scalar tails, row order and downstream column materialization retain their existing behavior.

The output selector already has one slot per input row. At a complete SIMD block, the number of emitted indices is at most the input position, so the fixed-width stores fit within that allocation; only the returned prefix is consumed. The lookup table has constant initialization and introduces no dynamic allocation or mutable shared state.

Add unit coverage and a standalone benchmark that measures the original loop and the actual production helper. On an Intel Xeon Platinum 8457C, with 4096-row batches, random 50% selection and Clang 20.1.8 -O3 -march=native:

Benchmark Before (ns/input row) After (ns/input row) Speedup
Row-index generation 0.693280 0.119248 5.81x
Predicate comparison + selection + 8-byte copy 1.170464 0.643518 1.82x

Performance tradeoff: Clang 16.0.6 with -march=x86-64-v3 regresses from 0.104319 to 0.119946 ns/input row at random 1% selection (about 15% slower). The Clang 20 native 1% case is approximately unchanged. These are standalone measurements; complete SQL query speedup has not been measured.

Release note

None

Check List (For Author)

  • Test
    • Regression test
    • Unit Test
    • Manual test (details below)

Validation completed:

  • Standalone benchmark with native and AVX2 compiler settings.
  • ASan + UBSan verification of all 256 byte masks, 65536 mixed masks, scalar tails, row order, output bounds, near-uint16 row limits and payload copies.
  • clang-format 16.0.6, repository build-hygiene checks and focused clang-tidy for the helper, test source and standalone benchmark.
  • Prepared this PR as one commit on the latest master; the helper, test and benchmark source blobs are identical to the verified versions.

Unit tests added: empty/full/single-bit masks, exhaustive byte and mixed masks, large row bases and consecutive appends. They have not executed through the standard BE test runner. Both ./run-be-ut.sh -j 48 --run --filter=SelectionTest.*:BitsTest.* and BUILD_TYPE=ASAN ./build.sh --be -j 48 were attempted in the development workspace, but compilation failed in unrelated existing local ColumnVector edits at column_vector.h:95 before the tests could execute. Full BE build and full SegmentIterator static analysis remain incomplete. No SQL or ARM execution.

Standalone reproduction (x86, from the repository root):

c++ -O3 -DNDEBUG -std=c++20 -march=native -Ibe/src \
    be/benchmark/selection_compaction_bench.cpp -o /tmp/selection_compaction_bench
/tmp/selection_compaction_bench --verify-only
/tmp/selection_compaction_bench --pattern random --rate 50 --mode select \
    --repeats 9 --min-ms 60
/tmp/selection_compaction_bench --pattern random --rate 50 --input values \
    --mode copy8 --repeats 9 --min-ms 80
  • 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?

@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

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

Reviewed PR #69818 at the verified head f94594d against base 8d88f1c. The change targets partial SIMD predicate-mask compaction with an x86 lookup table, while retaining scalar tails, full-mask handling, and non-x86 iteration. I found no verified P0-P3 issue and no existing blocking thread applies.

Critical checkpoints: the fixed-width lookup stores are bounded by the complete SIMD-block invariant and the selected-row vector is sized to the input batch; returned indices stay ascending and downstream short-circuit, row-id, output, and statistics paths consume only the returned count. Vector predicate producers write canonical boolean flags. The constexpr lookup table has no dynamic cross-TU lifecycle, and SegmentIterator state is not shared across concurrent callers. No configuration, protocol, storage-format, transaction, or lock-order changes are involved. The unit test is discovered by the existing BE util glob and covers masks, tails, maximum uint16 row positions, ordering, payload copies, and canaries; the standalone benchmark self-tests the same helper and is explicitly documented for x86.

The review was static-only as required by the review contract; no build or test execution is claimed. No additional user focus points were provided. Coverage and candidate dispositions are recorded in the shared ledger; all four risk focuses were dismissed with evidence, so the review converged in one round.

Review converged after 1 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17585	5519	5061	5061
q2	2112	322	283	283
q3	10205	1442	831	831
q4	4683	462	334	334
q5	7443	801	527	527
q6	189	181	151	151
q7	764	791	597	597
q8	9254	1349	1069	1069
q9	5521	4182	4328	4182
q10	6831	1290	1024	1024
q11	445	239	230	230
q12	640	401	277	277
q13	18091	4189	3072	3072
q14	283	275	247	247
q15	q16	726	730	647	647
q17	1207	755	582	582
q18	7092	6176	6128	6128
q19	1118	976	597	597
q20	385	336	228	228
q21	4871	2130	2289	2130
q22	354	293	250	250
Total cold run time: 99799 ms
Total hot run time: 28447 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5665	5597	5673	5597
q2	698	591	510	510
q3	5246	5346	4922	4922
q4	2017	2138	1603	1603
q5	5265	5134	5608	5134
q6	277	207	153	153
q7	2336	2140	1852	1852
q8	3020	2883	2680	2680
q9	7917	8033	7961	7961
q10	3831	3692	3258	3258
q11	639	455	446	446
q12	686	704	512	512
q13	3543	3986	3253	3253
q14	299	308	288	288
q15	q16	701	710	651	651
q17	8410	7544	7261	7261
q18	12914	12149	12875	12149
q19	889	770	784	770
q20	2151	2163	1906	1906
q21	5708	4664	4762	4664
q22	503	458	408	408
Total cold run time: 72715 ms
Total hot run time: 65978 ms

@hello-stephen

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

query5	4311	605	454	454
query6	424	207	193	193
query7	4807	442	231	231
query8	314	161	148	148
query9	8787	3196	3219	3196
query10	491	297	254	254
query11	5869	3848	3561	3561
query12	148	89	84	84
query13	1241	445	325	325
query14	6077	3949	3672	3672
query14_1	3444	3421	3362	3362
query15	212	199	179	179
query16	958	459	420	420
query17	1088	675	531	531
query18	2426	433	320	320
query19	200	165	145	145
query20	94	83	84	83
query21	206	127	108	108
query22	12976	13104	12785	12785
query23	13480	12925	12453	12453
query23_1	12539	12579	12612	12579
query24	7334	850	463	463
query24_1	473	481	494	481
query25	504	398	342	342
query26	1265	258	131	131
query27	2774	442	275	275
query28	4613	2033	2008	2008
query29	1564	613	436	436
query30	306	220	184	184
query31	1003	927	812	812
query32	147	88	87	87
query33	518	289	234	234
query34	907	872	513	513
query35	749	735	629	629
query36	801	830	717	717
query37	124	98	81	81
query38	1781	1785	1643	1643
query39	788	788	746	746
query39_1	716	732	736	732
query40	213	113	101	101
query41	65	63	61	61
query42	85	85	80	80
query43	341	354	307	307
query44	1346	705	707	705
query45	180	179	164	164
query46	857	978	571	571
query47	2930	2771	2667	2667
query48	296	317	220	220
query49	569	421	295	295
query50	715	294	219	219
query51	10523	10530	10395	10395
query52	79	80	68	68
query53	188	208	152	152
query54	236	186	186	186
query55	75	65	66	65
query56	225	210	208	208
query57	1651	1615	1554	1554
query58	282	265	256	256
query59	2094	2192	1983	1983
query60	298	248	229	229
query61	149	152	152	152
query62	389	326	297	297
query63	197	163	159	159
query64	2713	943	835	835
query65	3302	3276	3290	3276
query66	1780	417	298	298
query67	20257	20158	19991	19991
query68	3060	1033	632	632
query69	391	291	249	249
query70	881	783	801	783
query71	308	235	215	215
query72	2797	2599	2320	2320
query73	515	541	294	294
query74	4666	4489	4334	4334
query75	2353	2273	1897	1897
query76	2321	1041	630	630
query77	348	364	258	258
query78	9813	9574	9206	9206
query79	907	874	540	540
query80	497	424	353	353
query81	525	333	275	275
query82	213	137	107	107
query83	202	196	182	182
query84	269	122	94	94
query85	769	412	360	360
query86	257	235	236	235
query87	1999	1949	1810	1810
query88	3794	2782	2764	2764
query89	321	288	241	241
query90	2135	173	163	163
query91	168	149	124	124
query92	101	91	87	87
query93	1014	1030	587	587
query94	453	321	311	311
query95	586	328	300	300
query96	702	501	243	243
query97	2435	2439	2378	2378
query98	170	155	152	152
query99	768	761	647	647
Total cold run time: 233906 ms
Total hot run time: 151261 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.27	0.09	0.09
query4	1.61	0.10	0.10
query5	0.23	0.24	0.24
query6	1.17	0.69	0.68
query7	0.04	0.01	0.01
query8	0.05	0.04	0.03
query9	0.40	0.36	0.36
query10	0.57	0.58	0.60
query11	0.20	0.14	0.14
query12	0.18	0.15	0.14
query13	0.49	0.48	0.49
query14	1.03	1.03	1.01
query15	0.63	0.60	0.60
query16	0.36	0.35	0.35
query17	1.14	1.14	1.11
query18	0.20	0.20	0.20
query19	2.09	2.02	1.99
query20	0.02	0.01	0.01
query21	15.53	0.19	0.13
query22	4.83	0.05	0.05
query23	16.25	0.31	0.12
query24	3.04	0.55	0.40
query25	0.12	0.06	0.05
query26	1.47	0.23	0.15
query27	0.06	0.03	0.05
query28	3.78	0.77	0.38
query29	12.62	4.10	3.19
query30	0.28	0.12	0.09
query31	2.77	0.55	0.32
query32	3.22	0.59	0.50
query33	3.15	3.27	3.15
query34	15.38	4.00	3.36
query35	3.35	3.34	3.34
query36	0.56	0.46	0.44
query37	0.10	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.18	0.16	0.14
query41	0.08	0.03	0.03
query42	0.03	0.03	0.03
query43	0.05	0.03	0.03
Total cold run time: 97.72 s
Total hot run time: 24.23 s

### What problem does this PR solve?

Issue Number: None

Problem Summary: Partially selected SIMD masks are expanded one bit at a time,
creating a serial dependency for every selected row. Use a constexpr eight-row
lookup table and vector stores for x86 masks with more than two selected rows.
Keep the original ctz loop for sparse masks and other architectures. Preserve
zero/full-mask shortcuts, tails, row order, and downstream materialization.
The selector already has one slot per input row, so fixed-width stores fit in
that allocation even when only the returned prefix is valid.

On a Xeon Platinum 8457C, 4096-row batches and random 50% selection, the actual
helper reduces standalone compaction from 0.693280 to 0.119248 ns/input row
(5.81x, Clang 20 native). Including one int32 predicate comparison and an 8-byte
payload copy improves from 1.170464 to 0.643518 ns/input row (1.82x).
Tradeoff: the Clang 16 AVX2 random 1% compaction-only case changes from 0.104319
to 0.119946 ns/input row (15% slower); the Clang 20 native 1% case is unchanged.
These measurements do not establish complete SQL query speedups.

### Release note

None

### Check List (For Author)

- Test: Manual test: native and AVX2 standalone benchmark; ASan+UBSan exhaustive
  mask, tail, row-order, output-bounds and payload verification; clang-format 16;
  repository build-hygiene and focused clang-tidy checks.
  Add SelectionTest cases for masks, large row bases and consecutive appends.
  Standard BE UT and full ASAN BE build were attempted with -j 48, but failed
  in the unrelated existing local ColumnVector span-iterator/PODArray copy constructor at
  column_vector.h:95 before the new GTest cases could execute. No SQL test run.
- Behavior changed: No
- Does this need documentation: No
@HappenLee
HappenLee force-pushed the improvement/simd-selection-lookup branch from f94594d to ebea004 Compare October 30, 2026 01:44
@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

### What problem does this PR solve?

Related PR: apache#69818

Problem Summary: SelectionLookupTable stores an unsigned bit index in uint16_t,
and append_selected_indices stores an unsigned row-index expression in uint16_t.
Both implicit conversions fail BE compilation with -Wconversion -Werror. Cast
these two values explicitly. The bit index is in [0, 7], and the existing caller
contract keeps the row index within uint16_t. Selection behavior is unchanged.

### Release note

None

### Check List (For Author)

- Test: Manual test: Clang 20 and Clang 16 header compilation with -Wconversion
  -Werror; standalone exhaustive verification under ASAN/UBSAN; clang-format 16
  and build hygiene checks. Full BE build and BE UT were not run locally.
- Behavior changed: No
- Does this need documentation: No
@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

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

Reviewed PR #69818 at the verified exact head 08abb8f against base 6991e76. I found no verified P0-P3 issue and no current inline blocking thread applies. The fixed-width lookup stores are bounded by the complete SIMD-block invariant and the selection vector is sized to the input batch; uint16_t maximum positions remain representable and the scalar tail handles the remainder. The x86 lookup table matches the uint32 mask layout, while non-x86 builds retain the existing iterator fallback. Returned indices stay ascending, and downstream short-circuit, common-expression, row-id, output, and statistics paths consume only the returned prefix. The new unit test is discovered by the existing recursive BE test glob and covers exhaustive byte/mixed masks, tails, maximum positions, ordering, payloads, and canaries; the standalone benchmark dispatch and self-checks are internally consistent. Review was static-only as required; no builds or tests were executed. No additional user focus points were supplied. Existing blocking comment IDs: none.

Review did not converge after 3 rounds. This automated review cannot pass the review check.

The required delegated review lanes could not start or return results because each child launch terminated with the platform model-capacity error. The main agent completed three bounded static sweeps and independently covered all four changed paths and four recorded risks, but convergence cannot be claimed without the required latest-round subagent NO_NEW_VALUABLE_FINDINGS results.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17597	5167	5048	5048
q2	2143	309	249	249
q3	10264	1388	812	812
q4	4685	464	342	342
q5	7438	803	534	534
q6	184	185	152	152
q7	745	814	600	600
q8	9270	1328	1021	1021
q9	5469	4208	4174	4174
q10	6838	1285	1002	1002
q11	423	246	222	222
q12	655	394	279	279
q13	18100	3640	2989	2989
q14	274	280	250	250
q15	q16	743	724	674	674
q17	1200	742	588	588
q18	7037	6169	6116	6116
q19	1111	959	594	594
q20	385	328	234	234
q21	5349	2460	2419	2419
q22	393	339	279	279
Total cold run time: 100303 ms
Total hot run time: 28578 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6031	5902	6132	5902
q2	758	606	575	575
q3	5620	5825	5364	5364
q4	1999	2164	1607	1607
q5	5678	5790	5621	5621
q6	240	186	140	140
q7	2297	2031	1764	1764
q8	3008	2642	2658	2642
q9	7932	7491	7468	7468
q10	3630	3583	3186	3186
q11	585	425	387	387
q12	660	683	505	505
q13	3268	3579	3083	3083
q14	290	288	264	264
q15	q16	649	675	611	611
q17	7743	6945	6885	6885
q18	13013	12178	12898	12178
q19	924	839	784	784
q20	2144	2149	1925	1925
q21	5746	4693	4812	4693
q22	494	441	390	390
Total cold run time: 72709 ms
Total hot run time: 65974 ms

@hello-stephen

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

query5	4302	582	438	438
query6	423	203	189	189
query7	4823	441	232	232
query8	312	162	161	161
query9	8787	3210	3223	3210
query10	492	308	266	266
query11	6042	3849	3637	3637
query12	154	90	85	85
query13	1260	460	342	342
query14	6110	3933	3649	3649
query14_1	3405	3378	3416	3378
query15	208	197	181	181
query16	952	439	405	405
query17	865	645	531	531
query18	2419	432	318	318
query19	199	172	133	133
query20	88	83	82	82
query21	216	130	108	108
query22	12958	12915	12762	12762
query23	13434	12765	12401	12401
query23_1	12498	12504	12476	12476
query24	7363	828	480	480
query24_1	489	497	501	497
query25	537	418	357	357
query26	1269	266	141	141
query27	2728	456	274	274
query28	4602	2069	2066	2066
query29	1569	621	462	462
query30	304	220	187	187
query31	1013	923	815	815
query32	140	96	95	95
query33	539	308	244	244
query34	912	851	520	520
query35	747	755	642	642
query36	816	834	719	719
query37	140	101	84	84
query38	1801	1733	1701	1701
query39	799	793	747	747
query39_1	729	728	739	728
query40	224	128	106	106
query41	77	72	70	70
query42	89	87	84	84
query43	346	367	313	313
query44	1394	721	759	721
query45	173	172	162	162
query46	827	963	558	558
query47	2952	2971	2629	2629
query48	312	309	222	222
query49	571	394	295	295
query50	670	271	214	214
query51	10632	10363	10517	10363
query52	79	84	76	76
query53	199	201	148	148
query54	241	186	169	169
query55	70	61	63	61
query56	221	223	201	201
query57	1644	1620	1543	1543
query58	279	262	260	260
query59	2114	2147	1930	1930
query60	299	238	220	220
query61	146	148	146	146
query62	392	339	292	292
query63	197	170	157	157
query64	2718	932	810	810
query65	3307	3278	3260	3260
query66	1791	413	293	293
query67	20442	20004	20133	20004
query68	3166	1004	619	619
query69	400	290	286	286
query70	860	794	772	772
query71	316	239	207	207
query72	2945	2601	2259	2259
query73	535	540	298	298
query74	4676	4520	4354	4354
query75	2377	2263	1921	1921
query76	2404	1020	579	579
query77	368	361	271	271
query78	9742	9687	9222	9222
query79	1018	884	545	545
query80	1243	426	352	352
query81	582	321	269	269
query82	581	138	121	121
query83	286	199	182	182
query84	303	123	110	110
query85	1076	425	371	371
query86	368	241	232	232
query87	1981	1939	1805	1805
query88	3843	2847	2829	2829
query89	334	296	244	244
query90	1722	178	174	174
query91	156	157	140	140
query92	100	89	91	89
query93	1011	1022	582	582
query94	554	304	282	282
query95	579	413	300	300
query96	693	510	246	246
query97	2444	2420	2331	2331
query98	166	166	148	148
query99	787	758	646	646
Total cold run time: 236078 ms
Total hot run time: 151237 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 62.50% (15/24) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.23% (32558/48427)
Line Coverage 52.11% (344951/661951)
Region Coverage 47.52% (280234/589773)
Branch Coverage 49.24% (132446/268999)

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.09	0.05	0.04
query3	0.27	0.10	0.09
query4	1.61	0.10	0.12
query5	0.23	0.24	0.24
query6	1.17	0.68	0.68
query7	0.05	0.01	0.01
query8	0.06	0.04	0.03
query9	0.39	0.35	0.34
query10	0.58	0.57	0.58
query11	0.21	0.15	0.14
query12	0.18	0.14	0.14
query13	0.49	0.49	0.50
query14	1.05	1.02	1.01
query15	0.63	0.59	0.61
query16	0.34	0.33	0.35
query17	1.12	1.13	1.15
query18	0.21	0.20	0.19
query19	2.10	2.01	2.03
query20	0.02	0.02	0.01
query21	15.55	0.20	0.14
query22	4.90	0.06	0.06
query23	16.26	0.32	0.13
query24	2.78	0.99	0.46
query25	0.13	0.06	0.05
query26	1.68	0.22	0.16
query27	0.05	0.04	0.03
query28	3.31	0.80	0.37
query29	12.48	4.19	3.25
query30	0.28	0.14	0.09
query31	2.78	0.56	0.33
query32	3.22	0.59	0.51
query33	3.25	3.17	3.15
query34	15.38	3.96	3.40
query35	3.34	3.35	3.35
query36	0.55	0.42	0.41
query37	0.10	0.07	0.06
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.16	0.15	0.14
query41	0.09	0.03	0.03
query42	0.03	0.03	0.02
query43	0.04	0.04	0.03
Total cold run time: 97.27 s
Total hot run time: 24.38 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 62.50% (15/24) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.55% (36395/46929)
Line Coverage 62.80% (411299/654916)
Region Coverage 59.20% (347896/587634)
Branch Coverage 60.17% (161466/268341)

@HappenLee
HappenLee merged commit 9c8f7b2 into apache:master Oct 30, 2026
35 of 37 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

yiguolei pushed a commit that referenced this pull request Nov 6, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #65730 #66608 #66805 #66841 #66999 #67005 #67011 #67110
#67268 #67289 #67325 #67650 #67921 #68028 #68335 #68375 #68615 #68690
#68813 #68815 #68707 #68649

Problem Summary:

**In short.** This ports the Lance BE read path of branch-4.1 /
branch-4.2 to master, in its branch-4.2 form as of `cabcf45c1af`, plus
the BE side of three 4.x changes merged after that snapshot: the search
filter `prefilter` flag and profile layout (#68813), distance bounds for
`vector_search` (#68815), and the split-level index rule of #68707 that
#68815 relies on. It gathers the BE parts of the PRs listed above.
Master links `liblance_c` but has no Lance reader, so every
`FORMAT_LANCE` scan fails today. With this PR the BE reads Lance
datasets through FileScannerV2: plain scans, vector search and full-text
search, predicate and runtime-filter pushdown, the schema fetch of a
local Lance TVF, and the second phase of TopN lazy materialization. The
PR is BE only. The FE connector plugin and the regression suites come in
separate PRs, so until then nothing on master plans a Lance scan, and
the BE unit tests carry the proof.

Original authors: @zhangstar333 @Gabriel39 @wangzhaobo957-cloud @Jay-ju
@FANNG1

**Background.** Lance shipped in 4.1.4: the Lance catalog, `local()` /
`s3()` with `format=lance`, and the `vector_search` / `full_text_search`
TVFs. On master the FE side is being rebuilt as a connector plugin
(`fe-connector-lance`), and the BE needs the reader from 4.x. The thrift
ids are the branch-4.1 / 4.2 ones, including `TExternalSearchRequest.5`
(`prefilter`) and `TVectorSearchParams.6/7` (distance bounds) added
after the snapshot, so a BE built from master also reads the Lance scan
ranges and search requests that a 4.x FE sends during a rolling upgrade.
The one exception is `TVectorSearchOptions` 6, branch-4.2's
`approx_mode` (#69852, merged into branch-4.2 after the snapshot and not
ported): it is reserved in a comment, and a master BE ignores it, so
during a rolling upgrade a 4.2 FE that asks for `fast` or `accurate`
gets Lance's default (`normal`).

**The problem, and what it cost.**

| | master today | with this PR |
|---|---|---|
| A `FORMAT_LANCE` scan | FileScannerV2 has no Lance reader, so the scan
fails with "FileScannerV2 does not support file format" | read by
`LanceTableReader` in FileScannerV2 |
| `fetch_table_schema` for a local Lance TVF | not handled | returns the
Arrow schema mapped to Doris types; Null leaves keep `NULL_TYPE` |
| The row location of a Lance row in TopN lazy materialization |
`GlobalRowLoacationV2` only has a 32-bit row id, but a Lance row id is a
64-bit id within the snapshot | a version 1 layout carries the 64-bit
row id; phase two reads the rows with the dataset's `take_rows` |

**How this PR fixes it.**

- Reader: `be/src/format_v2/lance/*` and
`be/src/format_v2/table/lance_reader.*`, as on branch-4.2 except one
local renamed for master's `-Wshadow`. The unit tests construct
`RuntimeState(TQueryOptions(), globals)`, because master removed the
globals-only constructor.
- FileScannerV2: the reader dispatch and `_to_file_format` handle
`FORMAT_LANCE` / `"lance"`; `FileFormat::LANCE` is added to its two
exhaustive switches. Since #69723 every file scan uses FileScannerV2, so
nothing else routes Lance scans (on branch-4.2 a Lance query fails with
NotSupported when `enable_file_scanner_v2` is off).
- Row locations: `ROW_VERSION {FILE_LOCAL_ROW_ID = 0,
LANCE_DATASET_ROW_ID = 1}` and a union layout for
`GlobalRowLoacationV2`. Version 0 keeps its byte layout, which static
asserts pin. `PRequestBlockDesc.row_id` becomes `uint64` (a proto2
varint, so the wire format is compatible). The materialization operator
decodes row locations by version, rejects an unknown version or a wrong
size, and stamps `row_location_version`. `RowIdStorageReader` validates
the counts and the version, range-checks internal row ids against
`uint32`, and groups Lance rows by plan node, snapshot version and
dataset URI, so each dataset is read with one `take_rows`. Master's
segment batching, scan-slot dedup and fetch concurrency stay as they
are.
- Thrift (`PlanNodes.thrift`): the external search request body
(`TVectorElementType`, `TVectorMetric`, `TSearchVector`,
`TVectorSearchParams` 1-7, `TFtsCoverageMode`, `TFtsQueryType`,
`TFtsMatchOperator`, `TFullTextSearchParams`, `TSearchFilterFormat`,
`TSearchFilter`, `TVectorSearchOptions`, `TExternalSearchQuery`),
`TExternalSearchRequest` 2-5 and `TLanceFileDesc` 4-6, all with the
branch-4.1 / 4.2 ids. The enums spell out their 4.x values. No id is
reused. On master, #70075 reserved `TExternalSearchRequest` 2-5 in
comments and added 6 (`paimon_options`); this PR replaces those comments
with the fields and leaves 6 as it is. `TVectorSearchOptions` 6 is
reserved in a comment for branch-4.2's `approx_mode` (#69852).
- After the snapshot (one commit each):
- #68813: `prefilter` decides whether the TVF search filter runs before
(true) or after (unset / false) candidate selection, for vector and
full-text search. The profile groups the Lance counters under
`LanceTiming`, `LanceIO`, `LanceIndex`, `LanceVectorIndex`,
`LanceScalarIndex`, `LanceExecutionPlan`, `LanceScan`, `LanceSearchPlan`
and `LanceDataCache`, uses the 4.x counter names, and registers every
scan metric before the statistics callback is installed, so the
callback, which Lance invokes from its execution streams, never adds
counters. #68813's FE tweak that hides `TUnit.NONE` labels in
`AggCounter` belongs to the FE port.
- #68707 (part): a vector split searches exactly the index segments FE
planned for it, and a split without segments is searched flat whatever
`use_index` says; a split with segments under `use_index = false` is
rejected. 4.x FEs plan segments only when `use_index` allows them.
- #68815: `distance_lower_bound` (inclusive) and `distance_upper_bound`
(exclusive) are validated (finite FLOAT values, lower < upper at FLOAT
precision, single-vector only) and installed with
`lance_scanner_set_distance_range()`, which master's pinned lance-c
provides.
- Review fixes:
- A nested array goes to the Doris SerDes without a copy only when it
and every descendant start at offset 0 and reference exactly their child
ranges. Lance's legacy (v1) decoder can return a struct or list that is
tight at its own level over a Boolean child with a bit offset (a batch
that starts mid-byte in the bitmap), and the SerDes reject that under
Arrow input validation.
- A zero-field `STRUCT<>`, which Lance decodes, is reported as an
unsupported column at any nesting level, like the other unsupported
Lance types. It used to be typed as a STRUCT that `ColumnStruct` cannot
hold (its row count comes from its first child).
- A Lance Decimal256 field with precision <= 38 is narrowed to
Decimal128 by a safe Arrow cast before decoding. Doris types it
DECIMAL32/64/128I by precision, and those SerDes read only Decimal128
arrays; the BE used to crash on the first value, also for nested leaves.
Only values a reader can see are cast: before a nested leaf is narrowed,
every child slot that no visible row references (under a NULL list,
fixed-size-list, map or struct row, or outside a sliced parent) is
marked NULL, so a hidden value does not fail the cast and a visible
value outside the declared precision still does; map keys, which cannot
be NULL, stay checked.
- FLOAT / DOUBLE `>=` runtime filters stay residual-only. Lance compares
floats by totalOrder, so a pushed `x >= c` dropped negative-NaN rows
that Doris keeps.
- Runtime filters that mention INT64_MIN stay residual-only. The pinned
Lance SQL parser reads `-9223372036854775808` as the negation of a
magnitude that it parses as i64 first; the overflow turns the literal
into a Float64, which the planner cannot coerce to Int64 or Decimal, so
a pushed IN, `>=` or `<=` filter on an INT64 or scale-zero decimal
column failed planning and the query failed. Every other i64 is still
pushed.
- The shared Arrow DATE32 decoding
(`DataTypeDateV2SerDe::read_column_from_arrow`) rejects days outside
0000-01-01..9999-12-31 instead of returning a non-null `0000-00-00`,
skips null slots and adds the epoch in 64 bits: the DATE32 range part of
branch-4.1 / 4.2 #68649. It also changes ADBC, Arrow stream load and
paimon-rust reads.
- Child values hidden by a NULL list, fixed-size-list, map or struct row
are not decoded: the array, map and struct SerDes give the element SerDe
a validity bitmap that also clears those slots
(`mask_arrow_children_hidden_by_null_parents()`). Arrow leaves them
unspecified, and Lance's v2.1 miniblock encoding keeps them, so a hidden
10000-01-01 under a NULL fixed-size list used to fail the DATE32 range
check above. Visible values are unchanged; this applies to ADBC, Arrow
stream load and paimon-rust as well. The masks come from ExecEnv's
tracked Arrow pool. A nested Lance array that is compacted before
decoding is masked the same way, at every level, before its
`ValidateFull()`, so a hidden invalid UTF-8 string, out-of-precision
decimal, Date64 or time no longer fails the batch while a visible one
still does. Since #69697 the same masks keep hidden DATETIMEV2 /
TIMESTAMPTZ values out of its range checks. A mask costs at most one
pass per batch: a NULL row that owns no child slots, or only NULL ones,
allocates nothing, and Arrow stream load masks each retained IPC batch
once (`mask_arrow_descendants_not_visible()`) before it hands row ranges
to the SerDes, so its per-block reads allocate nothing. The one
per-block mask left is for map keys under a NULL map row that still owns
entries, since a MapArray cannot hold NULL keys; Arrow's map builder
writes NULL map rows without entries.
- Core: `to_protobuf` keeps `NULL_TYPE` for a NULL literal type, so a
Null leaf in ARRAY / MAP / STRUCT no longer comes back as BOOLEAN. The
nullable serde reads an Arrow Null array as NULLs without touching
buffers that the array does not have.
- Configs, caches and metrics, with the 4.x names and defaults:

  | Config | Default |
  |---|---|
  | `lance_index_cache_size_bytes` | 10 GiB |
  | `lance_metadata_cache_size_bytes` | 1 GiB |
  | `enable_lance_data_cache` | true |
  | `lance_data_cache_path` | `${DORIS_HOME}/lance_data_cache` |
  | `lance_data_cache_disk_capacity_bytes` | 100 GiB |
  | `lance_data_cache_read_block_size_bytes` | 1 MiB |
  | `lance_io_buffer_size_bytes` (mutable) | 2 GiB |
  | `lance_batch_readahead` (mutable) | 5 |
  | `lance_fragment_readahead` (mutable) | 5 |

The BE-wide Lance session (index cache, metadata cache and the optional
Foyer disk cache) is created by the first Lance scan, so a BE that never
reads Lance creates no cache directory. The first six configs take
effect after a restart. A negative cache size (and, while the data cache
is enabled, a negative disk capacity or read block size) fails the first
Lance scan with a message that names the setting, as does any data-cache
value lance-c rejects; the rest of the BE keeps serving. Ten
`lance_session_*` metrics report the two in-memory caches. With the
defaults, the disk tier keeps 51,200 sparse 2 MiB block files open from
the first Lance scan on (100 GiB in blocks of twice the read block
size), the same as on branch-4.1 / 4.2. Size
`lance_data_cache_disk_capacity_bytes` and
`lance_data_cache_read_block_size_bytes` against the BE's open-file
limit, or set `enable_lance_data_cache = false`.

The index and metadata caches live in lance-c memory: they are not
charged to a MemTracker or registered with CacheManager, so memory GC,
the cache capacity adjustment under memory pressure and
`/api/clear_cache/all` do not shrink them, and `enable_lance_data_cache
= false` does not turn them off. lance-c has no call to clear, evict or
resize them, so size them with the two configs. The disk tier keys
blocks by Lance's store prefix (`scheme$bucket` for S3, GCS and OSS,
without the endpoint) and the object path
(`thirdparty/patches/lance-c-foyer.patch`). Several endpoints that serve
the same bucket name and data-file path with different bytes (data files
registered under chosen names or replaced in place; Lance's writers use
random file names) share entries, also across a BE restart; such a
deployment should set `enable_lance_data_cache = false`. Both are as on
branch-4.1 / 4.2 and tracked as follow-ups.

A Lance scan's native working set, that is the I/O buffered up to
`lance_io_buffer_size_bytes` per scanner, the decode readahead and the
operators of a search plan such as the top-k of a flat vector search, is
allocated inside lance-c and is charged to neither the query nor its
workload group. It counts toward the BE process memory only, so memory
GC, which ranks queries by tracked memory, does not attribute it to the
Lance query. lance-c accepts no memory budget, so on BEs with little
memory headroom size `lance_io_buffer_size_bytes`,
`lance_batch_readahead` and `lance_fragment_readahead` for the number of
concurrent Lance scanners. As on branch-4.1 / 4.2; tracked as a
follow-up.
- Build: macOS links `-framework IOKit`. This is the first time objects
of `liblance_c.a` are pulled into `doris_be`, so the binary and the link
time grow. With this PR `util/rle_encoding.h` reaches 231 TUs and
`util/threadpool.h` 589, the same as master `315c2e00331` alone (limits
275 and 660 since #69717), and `gen_cpp/internal_service.pb.h` 562
(master alone 559, limit 660); the reach budgets are unchanged.

Not included: the Lance index metadata scanner (`LANCE_INDEX_ENTRIES`);
the FE side, including the rolling-upgrade guard that keeps Lance lazy
materialization off while an old BE could decode a version 1 row
location as version 0; the regression suites; the rest of #68707
(searching historical versions, tags and branches, rejecting fragment
ids missing from the snapshot, naming the snapshot in errors and the
profile). The `InitReaderAvgTime` / `GetBlockAvgTime` profile fix that
#67005 carried goes in a separate PR.

**Results.**

| | master | with this PR |
|---|---|---|
| A `FORMAT_LANCE` query | fails before reading anything | read by
`LanceTableReader` in FileScannerV2 |
| A Lance row in the second phase of TopN lazy materialization | cannot
be represented | a version 1 row location, read back with `take_rows` |
| A row location of an unknown version or size in phase two | decoded as
version 0 | rejected with an error |
| An Arrow DATE32 day outside 0000-01-01..9999-12-31 (ADBC, Arrow stream
load, paimon-rust, Lance) | read as a non-null `0000-00-00` | the read
fails with `InvalidArgument` |

BE unit tests (ASAN, macOS arm64, against master's pinned lance-c), on
the head rebased onto master `378404eeb12`: 1029 cases in 54 suites run,
1027 pass and 2 Parquet microbenchmarks skip by design. They cover the
Lance suites (107: `LanceTableReader*` 83, `LanceNestedNullTest` 10,
`LanceRuntimeFilterHelperTest` 10, `LanceSessionManagerTest` 4); the
row-location protocol (`MaterializationSharedStateTest`,
`RowIdStorageReaderTest`, and the new `RowIdFetchRequestValidationTest`:
count mismatch, unknown version, a Lance version on an internal mapping,
an internal row id of 2^32); `FileScannerV2Test` (28); the ORC and
Parquet reader suites (`NewOrcReaderTest`, `NewParquetReaderTest`,
`ParquetScanTest` and the other `Parquet*` suites), whose row ids now go
through `file_local`; and the Arrow / protobuf / DATE serde suites with
the new `ArrowParentNullsTest`, plus the Iceberg and Parquet reader
suites that decode Arrow dates. Reverting any of the review fixes, the
#68813 flag, the #68707 split rule or the #68815 bounds makes its new
tests fail. The sixth review round adds 13 cases (`ArrowParentNullsTest`
10, `LanceTableReaderTypeTest` 3) and extends one; with them the Arrow
SerDe, Lance, ADBC and `IcebergV2ReaderTest` suites run 317 cases: 310
pass and the 7 ADBC SQLite cases skip without a local SQLite driver. The
ninth review round adds
`LanceRuntimeFilterHelperTest.KeepsInt64MinFiltersResidualOnly` and
`LanceTableReaderFilterTest.KeepsInt64MinRuntimeFiltersOutOfLanceScanner`:
both pass with the 106 Lance cases (`Lance*`) on the branch rebased onto
`339e087b11f`, and restoring `parse_int64()` in either check makes
exactly these two fail. On the head rebased onto master `315c2e00331`,
with the per-block mask fix (`42cf4e3e5d1`), 328 cases in 24 suites
pass: the Arrow SerDe and validation suites, `ArrowParentNullsTest`
(15), `LoadArrowPipeTest` (47), the DATETIMEV2 / TIMESTAMPTZ SerDe
suites, `ArrowColumnToDorisColumnTest`, `ArrowBlockConvertorTest`,
`AdbcReaderTest`, `RemoteDorisV2ReaderTest`, `PaimonRustTableReaderTest`
and the Lance suites. Allocating at the first NULL row of each call
again fails the new `ArrowParentNullsTest` case and the four new
`LoadArrowPipeTest` block cases, dropping the per-batch mask fails the
LIST and STRUCT block cases, and returning the child unmasked fails the
new timestamp case, four `ArrowParentNullsTest` cases and
`LanceTableReaderTypeTest.IgnoresDate32PayloadHiddenByNullParents`.

### Release note

Add nine `lance_*` BE configs and `lance_session_*` BE metrics. They
take effect once the FE can plan Lance scans. Reading an Arrow DATE32
value outside 0000-01-01..9999-12-31 through ADBC, Arrow stream load or
paimon-rust now fails instead of returning 0000-00-00.

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] Unit Test
- `run-be-ut.sh --run --filter=...` with the suites listed under
Results: 420 of 420 pass on macOS arm64 (ASAN) on the content before the
rebases, and the Lance and Arrow SerDe suites pass again with the
nested-offset, zero-field struct, hidden-child, hidden-Decimal256 and
compaction-mask fixes (the ADBC SQLite cases skip without a local SQLite
driver). Rebased onto master `43f9ceb48d7`: #69668 conflicted in the
scanner selection and in `fetch_table_schema`, which now hands every
file format to `format::fetch_file_schema()`, so the Lance branch runs
ahead of that call; #69646 added a TopN row-group test that reads the
version 0 row location fields, which now go through `file_local` (the
earlier rebase onto `5caaeb3e3d4` resolved #69614 and #69601 in the
scanner selection, the `FileFormat` enum, its test and the reach budget
table). The 95 TUs that this PR changes or that master changed since
`5caaeb3e3d4` were syntax-checked against the rebased tree with the UT
build flags, and CI runs the suites again. Rebased again onto master
`8d88f1cc6f7`: the only conflict was the budget table, where #65858
moved the `util/rle_encoding.h` baseline to 828, and the 163 TUs that
this PR changes or that master changed since `43f9ceb48d7` pass the same
syntax check. Rebased again onto master `339e087b11f`: the only conflict
was the budget table again, where #67292 moved the `util/rle_encoding.h`
baseline to 834; #69872 added Arrow type checks ahead of the NULL-parent
masking in the map and struct readers and merged cleanly. Of the 114
sources that this PR changes or that master changed since `8d88f1cc6f7`,
111 pass the same syntax check. The other three are master's: two new
#67292 files under `storage/segment` fail the same way on master alone,
since they compile only inside their unity batch, and this PR adds no
source to that target; the #69818 benchmark is not in the UT build and
includes only `util/simd` headers that this PR does not touch. Reverting
any of the review fixes, the #68813 flag, the #68707 split rule or the
#68815 bounds makes its new tests fail. Rebased again onto master
`378404eeb12`, which removed the legacy file scanner (#69723): the
conflicts were in the scanner-selection code and tests that #69723
deleted, so this PR's Lance lines there went with them (including its
`LanceQueriesAlwaysUseScannerV2` test), in the profile keys of
`materialization_opertor.cpp`, and in the row fetch of
`rowid_fetcher.cpp`, whose non-Lance path is now only
`FileScannerV2::read_by_rows`; the deleted V1 ORC / Parquet row id tests
took this PR's `file_local` rename with them, and the
`util/rle_encoding.h` budget commit is dropped because master is no
longer near that limit. Of the 100 sources that this PR changes or that
master changed since `339e087b11f`, 99 pass the same syntax check; the
other, `format_v2/orc/orc_reader.cpp`, fails the same way on master
alone. On the rebased head the suites listed under Results run 1029
cases: 1027 pass and 2 microbenchmarks skip. The test-utility fix that
`DataTypeDateTimeV2SerDeTest` needed on macOS (#69695) is now on master.
Rebased again onto master `315c2e00331`: the only conflict was
`TExternalSearchRequest` in `PlanNodes.thrift`, where #70075 had
reserved fields 2-5 in comments; the two commits that add the fields
replace those comments, and field 6 stays #70075's `paimon_options`.
#69717 cut transitive includes from shared headers: of the 160 sources
that this PR changes or that master changed since `378404eeb12`, 156
pass the same syntax check; the two `jemalloc_hook` tests have no
compile command on macOS, `runtime/memory/jemalloc_hook.cpp` uses glibc
aliases that macOS cannot build (#69819), and
`format_v2/table_reader.cpp` alone fails the same way on master alone
while its unity batch compiles. The full `doris_be_test` builds and
links on the rebased head; `ArrowParentNulls*` and `Lance*` (121 cases)
pass before the mask fix, and the 328 cases above with it.
- `build-support/check-build-hygiene.sh` (on this branch rebased onto
master `315c2e00331`), clang-format 16 and
`build-support/run-clang-tidy.sh` on the changed files: all pass; on the
changed lines clang-tidy (llvm 22) reports only
`readability-inconsistent-ifelse-braces` on the `DORIS_CHECK` lines of
the masking functions, the macro's expansion that master's existing
`DORIS_CHECK`s report too.
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- Behavior changed:
    - [ ] No.
- [x] Yes. `PRequestBlockDesc.row_id` is `uint64`; phase two of TopN
lazy materialization rejects row locations of an unknown version or
size; `FORMAT_LANCE` scans run on FileScannerV2; an Arrow DATE32 day
outside the Doris DATE range fails the read instead of becoming
`0000-00-00`.
- Does this need documentation?
    - [x] No. Lance is documented with the FE connector.
- [ ] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->

---------

Co-authored-by: zhangstar333 <[email protected]>
Co-authored-by: Gabriel <[email protected]>
Co-authored-by: zhaobo wang <[email protected]>
Co-authored-by: jay <[email protected]>
Co-authored-by: FANNG <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: zy-kkk <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants