diff --git a/fe/fe-core/src/main/java/com/starrocks/qe/SessionVariable.java b/fe/fe-core/src/main/java/com/starrocks/qe/SessionVariable.java index 7e447f85fbd3c..e9fdf79798fde 100644 --- a/fe/fe-core/src/main/java/com/starrocks/qe/SessionVariable.java +++ b/fe/fe-core/src/main/java/com/starrocks/qe/SessionVariable.java @@ -356,6 +356,7 @@ public class SessionVariable implements Serializable, Writable, Cloneable { public static final String CBO_MAX_REORDER_NODE = "cbo_max_reorder_node"; public static final String CBO_PRUNE_SHUFFLE_COLUMN_RATE = "cbo_prune_shuffle_column_rate"; public static final String CBO_PUSH_DOWN_AGGREGATE_MODE = "cbo_push_down_aggregate_mode"; + public static final String CBO_ENABLE_HISTOGRAM_JOIN_ESTIMATION = "cbo_enable_histogram_join_estimation"; public static final String CBO_PUSH_DOWN_DISTINCT_BELOW_WINDOW = "cbo_push_down_distinct_below_window"; public static final String CBO_PUSH_DOWN_AGGREGATE = "cbo_push_down_aggregate"; @@ -1490,6 +1491,9 @@ public static MaterializedViewRewriteMode parse(String str) { @VarAttr(name = CBO_PUSH_DOWN_GROUPINGSET_RESHUFFLE, flag = VariableMgr.INVISIBLE) private boolean cboPushDownGroupingSetReshuffle = true; + @VarAttr(name = CBO_ENABLE_HISTOGRAM_JOIN_ESTIMATION, flag = VariableMgr.INVISIBLE) + private boolean cboEnableHistogramJoinEstimation = true; + @VariableMgr.VarAttr(name = PARSE_TOKENS_LIMIT) private int parseTokensLimit = 3500000; @@ -3479,6 +3483,10 @@ public void setCboPushDownDistinctBelowWindow(boolean flag) { this.cboPushDownDistinctBelowWindow = flag; } + public boolean isCboEnableHistogramJoinEstimation() { + return cboEnableHistogramJoinEstimation; + } + public boolean isCboPushDownDistinctBelowWindow() { return this.cboPushDownDistinctBelowWindow; } diff --git a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/BinaryPredicateStatisticCalculator.java b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/BinaryPredicateStatisticCalculator.java index e0eb1b198c045..1bb5fbfc549c2 100644 --- a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/BinaryPredicateStatisticCalculator.java +++ b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/BinaryPredicateStatisticCalculator.java @@ -16,6 +16,7 @@ package com.starrocks.sql.optimizer.statistics; import com.starrocks.analysis.BinaryType; +import com.starrocks.qe.ConnectContext; import com.starrocks.sql.optimizer.operator.scalar.BinaryPredicateOperator; import com.starrocks.sql.optimizer.operator.scalar.ColumnRefOperator; import com.starrocks.sql.optimizer.operator.scalar.ConstantOperator; @@ -295,15 +296,45 @@ public static Statistics estimateColumnToColumnComparison(ScalarOperator leftCol } } + /** + * Estimate selectivity based on domain contains assumption: + * selectivity = 1/max{NDV} + * It's not robust if the NDV is distorted, which usually lead to underestimated selectivity + */ + private static double estimateSelectivityWithNDV(ColumnStatistic leftColumnStatistic, + ColumnStatistic rightColumnStatistic) { + double leftDistinctValuesCount = leftColumnStatistic.getDistinctValuesCount(); + double rightDistinctValuesCount = rightColumnStatistic.getDistinctValuesCount(); + return 1.0 / Math.max(1, Math.max(leftDistinctValuesCount, rightDistinctValuesCount)); + } + + /** + * Estimate selectivity based on histogram: + * selectivity = sum{ overlap_area/total_area of all-buckets } + */ + private static Double estimateSelectivityWithHistogram(ColumnStatistic leftColumnStatistic, + ColumnStatistic rightColumnStatistic) { + ConnectContext context = ConnectContext.get(); + if (context == null || !context.getSessionVariable().isCboEnableHistogramJoinEstimation()) { + return null; + } + return HistogramEstimator.estimateEqualToSelectivity(leftColumnStatistic, rightColumnStatistic); + } + public static Statistics estimateColumnEqualToColumn(ScalarOperator leftColumn, ColumnStatistic leftColumnStatistic, ScalarOperator rightColumn, ColumnStatistic rightColumnStatistic, Statistics statistics, boolean isEqualForNull) { - double leftDistinctValuesCount = leftColumnStatistic.getDistinctValuesCount(); - double rightDistinctValuesCount = rightColumnStatistic.getDistinctValuesCount(); - double selectivity = 1.0 / Math.max(1, Math.max(leftDistinctValuesCount, rightDistinctValuesCount)); + double selectivity; + Double histogramSelectivity = estimateSelectivityWithHistogram(leftColumnStatistic, rightColumnStatistic); + if (histogramSelectivity != null) { + selectivity = histogramSelectivity; + } else { + selectivity = estimateSelectivityWithNDV(leftColumnStatistic, rightColumnStatistic); + } + double rowCount = statistics.getOutputRowCount() * selectivity * (isEqualForNull ? 1 : (1 - leftColumnStatistic.getNullsFraction()) * (1 - rightColumnStatistic.getNullsFraction())); diff --git a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Bucket.java b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Bucket.java index 80ad8f28a96e1..0c1169e515567 100644 --- a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Bucket.java +++ b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Bucket.java @@ -15,6 +15,8 @@ package com.starrocks.sql.optimizer.statistics; +import java.util.Objects; + public class Bucket { private final double lower; private final double upper; @@ -43,4 +45,28 @@ public Long getCount() { public Long getUpperRepeats() { return upperRepeats; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Bucket bucket = (Bucket) o; + return Double.compare(lower, bucket.lower) == 0 && Double.compare(upper, bucket.upper) == 0 && + Objects.equals(count, bucket.count) && + Objects.equals(upperRepeats, bucket.upperRepeats); + } + + @Override + public int hashCode() { + return Objects.hash(lower, upper, count, upperRepeats); + } + + @Override + public String toString() { + return String.format("[%f,%f,%d,%d]", lower, upper, count, upperRepeats); + } } \ No newline at end of file diff --git a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Histogram.java b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Histogram.java index 76596a8c9febc..af46600235748 100644 --- a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Histogram.java +++ b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/Histogram.java @@ -14,9 +14,12 @@ package com.starrocks.sql.optimizer.statistics; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.starrocks.sql.optimizer.operator.scalar.ConstantOperator; import com.starrocks.statistic.StatisticUtils; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -58,11 +61,33 @@ public String getMcvString() { sb.append("MCV: ["); mcv.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) .limit(printMcvSize) - .forEach(entry -> sb.append("[").append(entry.getKey()).append(":").append(entry.getValue()).append("]")); + .forEach(entry -> sb.append("[").append(entry.getKey()).append(":").append(entry.getValue()) + .append("]")); sb.append("]"); return sb.toString(); } + public List getOverlappedBuckets(double lower, double upper) { + int startIndex = Collections.binarySearch(buckets, new Bucket(lower, lower, 0L, 0L), + Comparator.comparingDouble(Bucket::getUpper)); + if (startIndex < 0) { + startIndex = -startIndex - 1; + } + + // Find the first bucket that overlaps with the upper bound + int endIndex = Collections.binarySearch(buckets, new Bucket(upper, upper, 0L, 0L), + Comparator.comparingDouble(Bucket::getLower)); + if (endIndex < 0) { + endIndex = -endIndex - 2; + } + + if (startIndex <= endIndex) { + return buckets.subList(startIndex, endIndex + 1); + } else { + return Lists.newArrayList(); + } + } + public Optional getRowCountInBucket(ConstantOperator constantOperator, double distinctValuesCount) { Optional valueOpt = StatisticUtils.convertStatisticsToDouble(constantOperator.getType(), constantOperator.toString()); @@ -86,9 +111,11 @@ public Optional getRowCountInBucket(ConstantOperator constantOperator, dou } if (constantOperator.getType().isFixedPointType()) { - rowCount = (long) Math.ceil(Math.max(1, rowCount / Math.max(1, (bucket.getUpper() - bucket.getLower())))); + rowCount = (long) Math.ceil( + Math.max(1, rowCount / Math.max(1, (bucket.getUpper() - bucket.getLower())))); } else { - rowCount = (long) Math.ceil(Math.max(1, rowCount / Math.max(1, distinctValuesCount / buckets.size()))); + rowCount = + (long) Math.ceil(Math.max(1, rowCount / Math.max(1, distinctValuesCount / buckets.size()))); } return Optional.of(rowCount); @@ -105,4 +132,28 @@ public Optional getRowCountInBucket(ConstantOperator constantOperator, dou return Optional.empty(); } + + static class Builder { + private final List buckets = Lists.newArrayList(); + private final Map mcv = Maps.newHashMap(); + + public Builder addBucket(Bucket bucket) { + this.buckets.add(bucket); + return this; + } + + public Builder addCommonValue(String key, Long count) { + this.mcv.put(key, count); + return this; + } + + public Histogram build() { + return new Histogram(buckets, mcv); + } + } + + @Override + public String toString() { + return "Histogram(buckets=" + buckets + ",mcv=" + mcv + ")"; + } } diff --git a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/HistogramEstimator.java b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/HistogramEstimator.java new file mode 100644 index 0000000000000..63a3fdc048f2c --- /dev/null +++ b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/HistogramEstimator.java @@ -0,0 +1,115 @@ +// Copyright 2021-present StarRocks, Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.starrocks.sql.optimizer.statistics; + +import com.google.common.base.Preconditions; +import org.apache.commons.collections.CollectionUtils; + +/** + * Use histogram to estimate cardinality + */ +public class HistogramEstimator { + + /** + * Estimate the selectivity of two columns with EqualTo operator + * Return null if fail to do the estimation + */ + public static Double estimateEqualToSelectivity(ColumnStatistic left, ColumnStatistic right) { + // Check if input parameters are valid + if (left == null || right == null) { + return null; + } + + // Get histograms + Histogram leftHistogram = left.getHistogram(); + Histogram rightHistogram = right.getHistogram(); + + // If either histogram is empty, estimation is not possible + if (leftHistogram == null || rightHistogram == null) { + return null; + } + if (CollectionUtils.isEmpty(leftHistogram.getBuckets()) && + CollectionUtils.isEmpty(rightHistogram.getBuckets())) { + return null; + } + + // Calculate the overlapping area of the two histograms + double overlapArea = 0.0; + double totalArea = 0.0; + + for (Bucket leftBucket : leftHistogram.getBuckets()) { + for (Bucket rightBucket : + rightHistogram.getOverlappedBuckets(leftBucket.getLower(), leftBucket.getUpper())) { + double overlap = calculateBucketOverlap(leftBucket, rightBucket); + overlapArea += overlap; + } + totalArea += leftBucket.getCount(); + } + + // Calculate selectivity + if (totalArea > 0) { + double selectivity = overlapArea / totalArea; + Preconditions.checkState(0.0 <= selectivity && selectivity <= 1.0, + "exceptional selectivity: " + selectivity); + return overlapArea / totalArea; + } else { + return null; + } + } + + private static double calculateBucketOverlap(Bucket leftBucket, Bucket rightBucket) { + double leftLower = leftBucket.getLower(); + double leftUpper = leftBucket.getUpper(); + double rightLower = rightBucket.getLower(); + double rightUpper = rightBucket.getUpper(); + + // Calculate overlap interval + double overlapLower = Math.max(leftLower, rightLower); + double overlapUpper = Math.min(leftUpper, rightUpper); + + // Calculate overlap ratio + double leftRange = leftUpper - leftLower; + double rightRange = rightUpper - rightLower; + double overlapRange = overlapUpper - overlapLower; + + double leftOverlapCount; + if (leftRange <= 0) { + leftOverlapCount = leftBucket.getUpperRepeats(); + } else { + double leftOverlapRatio = overlapRange / leftRange; + leftOverlapCount = leftBucket.getCount() * leftOverlapRatio; + // left: [lower, upper] + // right: [lower, upper] + // upper repeats should be excluded + if (leftUpper > rightUpper) { + leftOverlapCount -= leftBucket.getUpperRepeats() * leftOverlapRatio; + } + } + + double rightOverlapCount; + if (rightRange <= 0) { + rightOverlapCount = rightBucket.getUpperRepeats(); + } else { + double rightOverlapRatio = overlapRange / rightRange; + rightOverlapCount = rightBucket.getCount() * rightOverlapRatio; + if (leftUpper < rightUpper) { + rightOverlapCount -= rightBucket.getUpperRepeats() * rightOverlapRatio; + } + } + + // Estimate the count of overlapping elements + return Math.min(leftOverlapCount, rightOverlapCount); + } +} diff --git a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/StatisticsCalculator.java b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/StatisticsCalculator.java index 8e87cddafee3d..9109b29c54c99 100644 --- a/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/StatisticsCalculator.java +++ b/fe/fe-core/src/main/java/com/starrocks/sql/optimizer/statistics/StatisticsCalculator.java @@ -1109,8 +1109,8 @@ private Void computeJoinNode(ExpressionContext context, JoinOperator joinType, S Statistics rightStatistics = context.getChildStatistics(1); // construct cross join statistics Statistics.Builder crossBuilder = Statistics.builder(); - crossBuilder.addColumnStatisticsFromOtherStatistic(leftStatistics, context.getChildOutputColumns(0), false); - crossBuilder.addColumnStatisticsFromOtherStatistic(rightStatistics, context.getChildOutputColumns(1), false); + crossBuilder.addColumnStatisticsFromOtherStatistic(leftStatistics, context.getChildOutputColumns(0), true); + crossBuilder.addColumnStatisticsFromOtherStatistic(rightStatistics, context.getChildOutputColumns(1), true); double leftRowCount = leftStatistics.getOutputRowCount(); double rightRowCount = rightStatistics.getOutputRowCount(); double crossRowCount = StatisticUtils.multiplyRowCount(leftRowCount, rightRowCount); diff --git a/fe/fe-core/src/test/java/com/starrocks/sql/optimizer/statistics/HistogramEstimatorTest.java b/fe/fe-core/src/test/java/com/starrocks/sql/optimizer/statistics/HistogramEstimatorTest.java new file mode 100644 index 0000000000000..6a433862cad46 --- /dev/null +++ b/fe/fe-core/src/test/java/com/starrocks/sql/optimizer/statistics/HistogramEstimatorTest.java @@ -0,0 +1,206 @@ +// Copyright 2021-present StarRocks, Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.starrocks.sql.optimizer.statistics; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HistogramEstimatorTest { + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testEstimateEqualToSelectivity( + ColumnStatistic left, ColumnStatistic right, Double expectedSelectivity) { + Double actualSelectivity = HistogramEstimator.estimateEqualToSelectivity(left, right); + if (expectedSelectivity == null) { + Assertions.assertNull(actualSelectivity); + } else { + Assertions.assertNotNull(actualSelectivity); + assertEquals(expectedSelectivity, actualSelectivity, 0.01, "left histogram: " + left.getHistogram() + + "\nright histogram: " + right.getHistogram()); + } + } + + private static Stream provideTestCases() { + return Stream.of( + // Normal case: overlapping histograms + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + createColumnStatistic(new double[] {3, 7, 12}, new long[] {150, 250}), + 0.81), + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {1, 2}), + createColumnStatistic(new double[] {3, 7, 12}, new long[] {150, 250}), + 0.83), + Arguments.of( + createColumnStatistic(new double[] {3, 7, 12}, new long[] {150, 250}), + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + 0.61), + + // Normal case: diverse bucket + Arguments.of( + createColumnStatistic(new double[] {1, 100, 200, 300, 400}, new long[] {100, 200, 200, 400}), + createColumnStatistic(new double[] {1, 200, 400}, new long[] {150, 250}), + 0.44), + + // Normal case: lots of buckets, but the range is same + Arguments.of( + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 16)), + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 16)), + 1.0), + Arguments.of( + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 10)), + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 16)), + 1.0), + Arguments.of( + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 10)), + createColumnStatistic(createUniformedHistogram(800, 128, 1 << 16)), + 1.0), + Arguments.of( + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 10)), + createColumnStatistic(createUniformedHistogram(10, 10240, 1 << 16)), + 1.0), + + // low-cardinality single element histogram + Arguments.of( + createColumnStatistic(createSingleElementHistogram(100, 1024, 1 << 10)), + createColumnStatistic(createSingleElementHistogram(100, 1024, 1 << 10)), + 1.0), + Arguments.of( + createColumnStatistic(createSingleElementHistogram(100, 1024, 1 << 10)), + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 16)), + 0.0), + Arguments.of( + createColumnStatistic(createUniformedHistogram(100, 1024, 1 << 16)), + createColumnStatistic(createSingleElementHistogram(100, 1024, 1 << 10)), + 0.0), + + // Completely overlapping histograms + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + 1.0), + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {10, 20}), + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + 1.0), + + // Non-overlapping histograms + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + createColumnStatistic(new double[] {15, 20, 25}, new long[] {150, 250}), + 0.0), + + // One empty histogram + Arguments.of( + createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), + createColumnStatistic(), + null), + // Both empty histograms + Arguments.of(createColumnStatistic(), createColumnStatistic(), null), + // One null histogram + Arguments.of(createColumnStatistic(new double[] {1, 5, 10}, new long[] {100, 200}), null, null)); + } + + private static Histogram createUniformedHistogram(int numBuckets, double bucketRange, long perBucketCount) { + Histogram.Builder builder = new Histogram.Builder(); + double lower = 0.0; + for (int i = 0; i < numBuckets; i++) { + builder.addBucket(new Bucket(lower, lower + bucketRange, perBucketCount, 1L)); + lower += bucketRange; + } + return builder.build(); + } + + // upper == lower + private static Histogram createSingleElementHistogram(int numBuckets, double bucketRange, long perBucketCount) { + Histogram.Builder builder = new Histogram.Builder(); + double lower = 0.0; + for (int i = 0; i < numBuckets; i++) { + builder.addBucket(new Bucket(lower, lower, perBucketCount, perBucketCount)); + lower += bucketRange; + } + return builder.build(); + } + + // create an empty column statistics + private static ColumnStatistic createColumnStatistic() { + return new ColumnStatistic(0, 0, 0, 0, 0, null, ColumnStatistic.StatisticType.ESTIMATE); + } + + private static ColumnStatistic createColumnStatistic(Histogram hist) { + return new ColumnStatistic(0, 0, 0, 0, 0, hist, ColumnStatistic.StatisticType.ESTIMATE); + } + + private static ColumnStatistic createColumnStatistic(double[] bounds, long[] counts) { + Histogram.Builder builder = new Histogram.Builder(); + for (int i = 0; i < counts.length; i++) { + builder.addBucket(new Bucket(bounds[i], bounds[i + 1], counts[i], 0L)); + } + Histogram histogram = builder.build(); + return new ColumnStatistic(0, 0, 0, 0, 0, histogram, ColumnStatistic.StatisticType.ESTIMATE); + } + + private List verifyBucketIndex(Histogram histogram, List buckets) { + return buckets.stream().map(x -> histogram.getBuckets().indexOf(x)).collect(Collectors.toList()); + } + + @Test + public void testGetOverlappedBuckets() { + Histogram histogram = new Histogram( + Lists.newArrayList( + new Bucket(0, 5, 100L, 0L), + new Bucket(5, 10, 200L, 0L), + new Bucket(10, 15, 300L, 0L), + new Bucket(15, 20, 400L, 0L) + ), + Maps.newHashMap() + ); + + // totally covered range + assertEquals(List.of(0, 1, 2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(0, 100))); + assertEquals(List.of(0, 1, 2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(2, 16))); + assertEquals(List.of(0, 1, 2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(0, 15))); + assertEquals(List.of(0, 1, 2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(5, 17))); + + // partially covered + assertEquals(List.of(1, 2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(10, 17))); + assertEquals(List.of(0, 1), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(1, 6))); + assertEquals(List.of(0, 1), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(0, 5))); + assertEquals(List.of(0, 1, 2), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(0, 10))); + assertEquals(List.of(2, 3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(15, 20))); + + // boundary overlapped + assertEquals(List.of(0), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(-1, 0))); + assertEquals(List.of(3), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(20, 21))); + assertEquals(List.of(0, 1), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(5, 5))); + + // no overlap + assertEquals(List.of(), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(30, 100))); + assertEquals(List.of(), verifyBucketIndex(histogram, histogram.getOverlappedBuckets(-10, -1))); + } + +}