1717
1818package org .apache .doris .datasource .lance .source ;
1919
20+ import org .apache .doris .analysis .BinaryPredicate ;
2021import org .apache .doris .analysis .CompoundPredicate ;
2122import org .apache .doris .analysis .Expr ;
23+ import org .apache .doris .analysis .InPredicate ;
24+ import org .apache .doris .analysis .IsNullPredicate ;
2225import org .apache .doris .analysis .SlotRef ;
2326import org .apache .doris .datasource .lance .index .LanceIndexSegmentGroup ;
2427import org .apache .doris .datasource .lance .index .LanceIndexSegmentInfo ;
@@ -61,6 +64,8 @@ static Plan plan(LanceTableMetadata metadata, List<Expr> pushedConjuncts,
6164 // provides a stable winner when multiple indices cover the same number of rows.
6265 List <LanceIndexSegmentGroup > indices = new ArrayList <>(metadata .getIndexes ());
6366 indices .sort (java .util .Comparator .comparing (LanceIndexSegmentGroup ::getName ));
67+ boolean splitComplementByFragment = pushedConjuncts .stream ()
68+ .anyMatch (LanceScalarIndexPlanner ::containsComplement );
6469 Plan selected = null ;
6570 for (LanceIndexSegmentGroup logicalIndex : indices ) {
6671 List <LanceIndexSegmentInfo > segments = logicalIndex .getSegments ();
@@ -72,7 +77,8 @@ static Plan plan(LanceTableMetadata metadata, List<Expr> pushedConjuncts,
7277 || index .getFieldIds ().size () != 1 || !filterFields .contains (index .getFieldIds ().get (0 ))) {
7378 continue ;
7479 }
75- Plan candidate = groupFragments (metadata , segments , visibleFragments );
80+ Plan candidate = groupFragments (metadata , segments , visibleFragments ,
81+ splitComplementByFragment );
7682 if (candidate != null && (selected == null || candidate .coveredRows > selected .coveredRows )) {
7783 selected = candidate ;
7884 }
@@ -115,8 +121,20 @@ private static Set<Integer> collectDriverFields(LanceTableMetadata metadata, Exp
115121 return fields ;
116122 }
117123
124+ private static boolean containsComplement (Expr expr ) {
125+ if ((expr instanceof CompoundPredicate
126+ && ((CompoundPredicate ) expr ).getOp () == CompoundPredicate .Operator .NOT )
127+ || (expr instanceof InPredicate && ((InPredicate ) expr ).isNotIn ())
128+ || (expr instanceof BinaryPredicate
129+ && ((BinaryPredicate ) expr ).getOp () == BinaryPredicate .Operator .NE )
130+ || (expr instanceof IsNullPredicate && ((IsNullPredicate ) expr ).isNotNull ())) {
131+ return true ;
132+ }
133+ return expr .getChildren ().stream ().anyMatch (LanceScalarIndexPlanner ::containsComplement );
134+ }
135+
118136 private static Plan groupFragments (LanceTableMetadata metadata , List <LanceIndexSegmentInfo > segments ,
119- Map <Long , LanceFragmentInfo > visibleFragments ) {
137+ Map <Long , LanceFragmentInfo > visibleFragments , boolean splitComplementByFragment ) {
120138 LanceSplitBuilder splits = new LanceSplitBuilder (
121139 metadata .getDatasetUri (), metadata .getVersion (), segments .size ());
122140 Set <Long > coveredFragments = new HashSet <>();
@@ -144,7 +162,17 @@ private static Plan groupFragments(LanceTableMetadata metadata, List<LanceIndexS
144162 coveredRows += Math .max (fragment .getPhysicalRows (), 0 );
145163 }
146164 if (!fragments .isEmpty ()) {
147- splits .addIndexSegmentSplit (segment .getUuid (), fragments , physicalRows );
165+ // Complement selectivity is unknown. Preserve fragment-level scheduling so a
166+ // broad NOT cannot funnel all covered data through one BE. Each task retains
167+ // the same segment UUID but owns a disjoint domain, including on fallback.
168+ if (splitComplementByFragment ) {
169+ for (Long fragmentId : fragments ) {
170+ splits .addIndexSegmentSplit (segment .getUuid (), java .util .Collections .singletonList (fragmentId ),
171+ visibleFragments .get (fragmentId ).getSchedulingWeight ());
172+ }
173+ } else {
174+ splits .addIndexSegmentSplit (segment .getUuid (), fragments , physicalRows );
175+ }
148176 }
149177 }
150178 return splits .isEmpty () ? null : new Plan (segments .get (0 ).getIndexName (), splits , coveredRows );
0 commit comments