Skip to content

Commit febc1c3

Browse files
mergify[bot]xiangguangyxgclaude
authored
[Enhancement] Align range-colocate scan-range dispatch by ColocateRange index (backport #72278) (#72804)
Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com> Co-authored-by: xiangguangyxg <110401425+xiangguangyxg@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2b9a7d9 commit febc1c3

11 files changed

Lines changed: 1147 additions & 9 deletions

File tree

fe/fe-core/src/main/java/com/starrocks/catalog/ColocateRangeMgr.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,39 @@ public List<ColocateRange> getColocateRanges(long colocateGroupId) {
6666
* @return the ColocateRange containing the value, or null if not found
6767
*/
6868
public ColocateRange getColocateRange(long colocateGroupId, Tuple colocateValue) {
69+
int index = getColocateRangeIndex(colocateGroupId, colocateValue);
70+
if (index < 0) {
71+
return null;
72+
}
73+
return colocateGroupToRanges.get(colocateGroupId).get(index);
74+
}
75+
76+
/**
77+
* Returns the index of the ColocateRange that contains the given colocate value.
78+
*
79+
* <p>The index is stable across all tables sharing this colocate group: the i-th
80+
* range in {@link #getColocateRanges(long)} corresponds to the same PACK shard group
81+
* across every partition/table/database in the group. Coordinator-side scan-range
82+
* dispatch uses this index as the bucket sequence so that scan ranges from joined
83+
* tables that share a colocate range are routed to the same fragment instance.
84+
*
85+
* @param colocateGroupId the colocate group id
86+
* @param colocateValue the colocate prefix to look up; null means the global lower
87+
* bound (-inf), which always lands in the first range
88+
* @return the index in {@code [0, getColocateRanges(grpId).size())}, or -1 if the
89+
* group does not exist or the value is not covered
90+
*/
91+
public int getColocateRangeIndex(long colocateGroupId, Tuple colocateValue) {
6992
List<ColocateRange> colocateRanges = colocateGroupToRanges.get(colocateGroupId);
7093
if (colocateRanges == null || colocateRanges.isEmpty()) {
71-
return null;
94+
return -1;
95+
}
96+
if (colocateValue == null) {
97+
return 0;
7298
}
7399
Range<Tuple> pointRange = Range.gele(colocateValue, colocateValue);
74100
int index = Collections.binarySearch(colocateRanges, pointRange);
75-
return index >= 0 ? colocateRanges.get(index) : null;
101+
return index >= 0 ? index : -1;
76102
}
77103

78104
/**

fe/fe-core/src/main/java/com/starrocks/catalog/ColocateRangeUtils.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,43 @@ private static Tuple extendTupleWithNull(Tuple tuple, List<Column> sortKeyColumn
8383
}
8484
return new Tuple(values);
8585
}
86+
87+
/**
88+
* Extracts the colocate column prefix from a tablet's full sort-key range.
89+
*
90+
* <p>Inverse of {@link #expandToFullSortKey}: a tablet whose range was produced by
91+
* expansion stores a full sort-key tuple in its lower bound, but the colocate-range
92+
* lookup keys on the colocate prefix only.
93+
*
94+
* <p>If the tablet range is unbounded below (lower bound is -inf), this returns
95+
* {@code null}, signaling the caller to fall back to the first colocate range
96+
* (which always begins at -inf by the {@link ColocateRangeMgr} invariant).
97+
*
98+
* <p>Unlike {@link #expandToFullSortKey}, this method requires
99+
* {@code colocateColumnCount > 0}: a colocate group with zero colocate columns
100+
* is not a meaningful concept on the lookup side (every value would map to the
101+
* same range, which is just the no-colocate case).
102+
*
103+
* @param tabletRange the tablet's full sort-key range
104+
* @param colocateColumnCount the number of colocate columns (sort key prefix length),
105+
* must be positive
106+
* @return the colocate prefix Tuple, or {@code null} if the range is unbounded below
107+
*/
108+
public static Tuple extractColocatePrefix(Range<Tuple> tabletRange, int colocateColumnCount) {
109+
Preconditions.checkArgument(colocateColumnCount > 0,
110+
"colocateColumnCount must be positive, got %s", colocateColumnCount);
111+
if (tabletRange.isMinimum()) {
112+
return null;
113+
}
114+
List<Variant> values = tabletRange.getLowerBound().getValues();
115+
Preconditions.checkArgument(values.size() >= colocateColumnCount,
116+
"tablet lower bound has %s values, fewer than colocateColumnCount %s",
117+
values.size(), colocateColumnCount);
118+
if (values.size() == colocateColumnCount) {
119+
return tabletRange.getLowerBound();
120+
}
121+
// subList returns a view backed by the original list; copy so that the
122+
// returned Tuple does not retain a reference to the tablet's bound.
123+
return new Tuple(new ArrayList<>(values.subList(0, colocateColumnCount)));
124+
}
86125
}

fe/fe-core/src/main/java/com/starrocks/common/Range.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,53 @@ public boolean isOverlapping(Range<T> other) {
346346
return !(isLessThan(other) || isGreaterThan(other));
347347
}
348348

349+
/**
350+
* Checks if this range contains every value of another range.
351+
*
352+
* <p>Handles {@code -∞} / {@code +∞} bounds and inclusive/exclusive edges
353+
* explicitly. Unlike {@link #compareTo(Range)}, which returns 0 for any
354+
* overlapping pair, this returns true only when {@code other} is a subset
355+
* of {@code this}.
356+
*
357+
* @param other the candidate sub-range, must not be null
358+
* @return true if every value in {@code other} is also in this range
359+
* @throws NullPointerException if other is null
360+
*/
361+
public boolean contains(Range<T> other) {
362+
Objects.requireNonNull(other, "Range must not be null");
363+
return lowerBoundContainsLowerOf(other) && upperBoundContainsUpperOf(other);
364+
}
365+
366+
private boolean lowerBoundContainsLowerOf(Range<T> other) {
367+
if (isMinimum()) {
368+
return true;
369+
}
370+
if (other.isMinimum()) {
371+
return false;
372+
}
373+
int cmp = getLowerBound().compareTo(other.getLowerBound());
374+
if (cmp != 0) {
375+
return cmp < 0;
376+
}
377+
// Equal lower bound values: this range must include the bound, or
378+
// other must also exclude it.
379+
return isLowerBoundIncluded() || !other.isLowerBoundIncluded();
380+
}
381+
382+
private boolean upperBoundContainsUpperOf(Range<T> other) {
383+
if (isMaximum()) {
384+
return true;
385+
}
386+
if (other.isMaximum()) {
387+
return false;
388+
}
389+
int cmp = other.getUpperBound().compareTo(getUpperBound());
390+
if (cmp != 0) {
391+
return cmp < 0;
392+
}
393+
return isUpperBoundIncluded() || !other.isUpperBoundIncluded();
394+
}
395+
349396
/**
350397
* Compares this range with another for ordering.
351398
*

fe/fe-core/src/main/java/com/starrocks/planner/OlapScanNode.java

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@
131131
import java.util.stream.Collectors;
132132
import java.util.stream.IntStream;
133133

134+
import javax.annotation.Nullable;
135+
134136
public class OlapScanNode extends AbstractOlapTableScanNode {
135137
private static final Logger LOG = LogManager.getLogger(OlapScanNode.class);
136138

@@ -316,7 +318,12 @@ public List<Expr> getBucketExprs() {
316318

317319
@Override
318320
public int getBucketNums() {
319-
int bucketNum = olapTable.getDefaultDistributionInfo().getBucketNum();
321+
DistributionInfo distInfo = olapTable.getDefaultDistributionInfo();
322+
if (distInfo.getType() == DistributionInfo.DistributionInfoType.RANGE) {
323+
return getRangeDistributionBucketNums(distInfo);
324+
}
325+
// HASH path.
326+
int bucketNum = distInfo.getBucketNum();
320327
if (getSelectedPartitionIds().size() <= 1) {
321328
for (Long pid : getSelectedPartitionIds()) {
322329
bucketNum = olapTable.getPartition(pid).getDistributionInfo().getBucketNum();
@@ -325,6 +332,54 @@ public int getBucketNums() {
325332
return bucketNum;
326333
}
327334

335+
private int getRangeDistributionBucketNums(DistributionInfo distInfo) {
336+
RangeColocateScanDispatch dispatch = RangeColocateScanDispatch.forTable(olapTable);
337+
if (dispatch != null) {
338+
// getBucketNums() is invoked from ExecutionFragment.getOrCreateColocatedAssignment
339+
// only when BackendSelectorFactory has chosen a colocate-dispatch path. Verify
340+
// alignment HERE, after the dispatch decision: a misaligned ColocateRangeMgr
341+
// would silently produce wrong join results under colocate dispatch.
342+
// Non-colocate scans go through NormalBackendSelector and never reach this point.
343+
dispatch.requireAligned(getSelectedPhysicalPartitions(), index.indexMetaId);
344+
return dispatch.bucketCount();
345+
}
346+
// Range distribution without a colocate group: RangeDistributionInfo always
347+
// reports 1 (one tablet per partition by design).
348+
return distInfo.getBucketNum();
349+
}
350+
351+
/**
352+
* Returns the physical partitions that actually contribute scan tablets.
353+
* Optimizer path: derived from {@link #partitionToScanTabletMap} (skip
354+
* empty entries — those were pruned out). Legacy path: every sub-partition
355+
* of every selected logical partition.
356+
*/
357+
private List<PhysicalPartition> getSelectedPhysicalPartitions() {
358+
List<PhysicalPartition> result = new ArrayList<>();
359+
if (partitionToScanTabletMap != null) {
360+
for (Map.Entry<Long, List<Long>> entry : partitionToScanTabletMap.entrySet()) {
361+
if (entry.getValue() == null || entry.getValue().isEmpty()) {
362+
continue;
363+
}
364+
PhysicalPartition physicalPartition = olapTable.getPhysicalPartition(entry.getKey());
365+
if (physicalPartition != null) {
366+
result.add(physicalPartition);
367+
}
368+
}
369+
return result;
370+
}
371+
for (Long partitionId : selectedPartitionIds) {
372+
Partition partition = olapTable.getPartition(partitionId);
373+
if (partition == null) {
374+
continue;
375+
}
376+
for (PhysicalPartition physicalPartition : partition.getSubPartitions()) {
377+
result.add(physicalPartition);
378+
}
379+
}
380+
return result;
381+
}
382+
328383
@Override
329384
public Optional<List<BucketProperty>> getBucketProperties() throws StarRocksException {
330385
return Optional.empty();
@@ -763,6 +818,11 @@ private void computeTabletInfo() throws StarRocksException {
763818
*/
764819
Preconditions.checkState(scanBackendIds.size() == 0);
765820
Preconditions.checkState(scanTabletIds.size() == 0);
821+
DistributionInfo distInfo = olapTable.getDefaultDistributionInfo();
822+
RangeColocateScanDispatch dispatch = null;
823+
if (distInfo.getType() == DistributionInfo.DistributionInfoType.RANGE) {
824+
dispatch = RangeColocateScanDispatch.forTable(olapTable);
825+
}
766826
for (Long partitionId : selectedPartitionIds) {
767827
final Partition partition = olapTable.getPartition(partitionId);
768828

@@ -783,16 +843,37 @@ private void computeTabletInfo() throws StarRocksException {
783843
scanTabletIds.addAll(allTabletIds);
784844
}
785845

786-
for (int i = 0; i < allTabletIds.size(); i++) {
787-
tabletId2BucketSeq.put(allTabletIds.get(i), i);
788-
}
846+
fillTabletId2BucketSeq(dispatch, selectedIndex, allTabletIds);
789847
totalTabletsNum += selectedIndex.getTablets().size();
790848
selectedTabletsNum += tablets.size();
791849
addScanRangeLocations(partition, physicalPartition, selectedIndex, tablets, localBeId);
792850
}
793851
}
794852
}
795853

854+
/**
855+
* Populates {@link #tabletId2BucketSeq} for one {@link MaterializedIndex}.
856+
* Range-colocate scans use the bucket sequence supplied by the dispatch
857+
* facade when alignment holds; everything else (HASH, range non-colocate,
858+
* or transiently unaligned range colocate) falls back to position-based
859+
* bucketSeq. The dispatch-time alignment guard fires later in
860+
* {@link #getBucketNums()} for the colocate-dispatch path.
861+
*/
862+
private void fillTabletId2BucketSeq(@Nullable RangeColocateScanDispatch dispatch,
863+
MaterializedIndex selectedIndex,
864+
List<Long> allTabletIds) {
865+
if (dispatch != null) {
866+
Map<Long, Integer> rangeColocateMap = dispatch.computeBucketSeq(selectedIndex);
867+
if (rangeColocateMap != null) {
868+
tabletId2BucketSeq.putAll(rangeColocateMap);
869+
return;
870+
}
871+
}
872+
for (int i = 0; i < allTabletIds.size(); i++) {
873+
tabletId2BucketSeq.put(allTabletIds.get(i), i);
874+
}
875+
}
876+
796877
public void checkIfScanRangeNumSafe(long scanRangeSize) {
797878
long totalPartitionNum = 0;
798879
long totalTabletsNum = 0;

0 commit comments

Comments
 (0)