-
Notifications
You must be signed in to change notification settings - Fork 4.8k
HIVE-29652:Improve range selectivity estimations in the absence of histograms #6746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -484,19 +484,261 @@ | |
| } | ||
|
|
||
| final List<ColStatistics> colStats = scan.getColStat(Collections.singletonList(inputRefIndex)); | ||
| if (colStats.isEmpty() || !isHistogramAvailable(colStats.get(0))) { | ||
| if (colStats.isEmpty()) { | ||
| return defaultSelectivity.get(); | ||
| } | ||
|
|
||
| final KllFloatsSketch kll = KllFloatsSketch.heapify(Memory.wrap(colStats.get(0).getHistogram())); | ||
| double rawSelectivity = rangedSelectivity(kll, boundaries); | ||
| final ColStatistics cs = colStats.get(0); | ||
| if (isHistogramAvailable(cs)) { | ||
| final KllFloatsSketch kll = KllFloatsSketch.heapify(Memory.wrap(cs.getHistogram())); | ||
| double rawSelectivity = rangedSelectivity(kll, boundaries); | ||
| if (inverseBool) { | ||
| // when inverseBool == true, this is a NOT_BETWEEN and selectivity must be inverted | ||
| // if there's a cast, the inversion is with respect to its codomain (range of the values of the cast) | ||
| double typeRangeSelectivity = rangedSelectivity(kll, typeRange); | ||
| rawSelectivity = typeRangeSelectivity - rawSelectivity; | ||
| } | ||
| return scaleSelectivityToNullableValues(kll, rawSelectivity, scan); | ||
| } | ||
|
|
||
| if (isUniformWithinRangeEnabled() && hasUsableMinMax(cs)) { | ||
| RelDataType columnType = scan.getRowType().getFieldList().get(inputRefIndex).getType(); | ||
| Double uniformSelectivity = computeUniformRangeSelectivity(cs, boundaries, scan, inverseBool, typeRange, | ||
| columnType); | ||
| if (uniformSelectivity != null) { | ||
| return uniformSelectivity; | ||
| } | ||
| } | ||
|
|
||
| return defaultSelectivity.get(); | ||
| } | ||
|
|
||
| private boolean isUniformWithinRangeEnabled() { | ||
| HiveConfPlannerContext ctx = | ||
| childRel.getCluster().getPlanner().getContext().unwrap(HiveConfPlannerContext.class); | ||
| return ctx == null || ctx.isUniformWithinRange(); | ||
| } | ||
|
|
||
| private static boolean hasUsableMinMax(ColStatistics cs) { | ||
| ColStatistics.Range range = cs.getRange(); | ||
| return range != null && range.minValue != null && range.maxValue != null; | ||
| } | ||
|
|
||
| /** | ||
| * Converts column MIN/MAX statistics into the same numeric space used by {@link #extractLiteral}. | ||
| * DATE column stats from HMS are stored as days since epoch; literals use epoch seconds. | ||
| */ | ||
| private static Optional<float[]> convertColRangeToFloatBounds(ColStatistics cs, RelDataType columnType) { | ||
| ColStatistics.Range range = cs.getRange(); | ||
| if (range == null || range.minValue == null || range.maxValue == null) { | ||
| return Optional.empty(); | ||
| } | ||
| final float min; | ||
| final float max; | ||
| switch (columnType.getSqlTypeName()) { | ||
| case DATE: | ||
| min = range.minValue.longValue() * 86400L; | ||
|
Check warning on line 540 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| max = range.maxValue.longValue() * 86400L; | ||
|
Check warning on line 541 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| break; | ||
| case TIMESTAMP: | ||
| min = range.minValue.longValue(); | ||
| max = range.maxValue.longValue(); | ||
| break; | ||
| case TINYINT: | ||
| min = range.minValue.byteValue(); | ||
| max = range.maxValue.byteValue(); | ||
| break; | ||
| case SMALLINT: | ||
| min = range.minValue.shortValue(); | ||
| max = range.maxValue.shortValue(); | ||
| break; | ||
| case INTEGER: | ||
| min = range.minValue.intValue(); | ||
| max = range.maxValue.intValue(); | ||
| break; | ||
| case BIGINT: | ||
| min = range.minValue.longValue(); | ||
| max = range.maxValue.longValue(); | ||
| break; | ||
|
Check warning on line 562 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| case FLOAT: | ||
| min = range.minValue.floatValue(); | ||
| max = range.maxValue.floatValue(); | ||
| break; | ||
| case DOUBLE: | ||
| min = (float) range.minValue.doubleValue(); | ||
| max = (float) range.maxValue.doubleValue(); | ||
| break; | ||
| case DECIMAL: | ||
| min = new BigDecimal(range.minValue.toString()).floatValue(); | ||
| max = new BigDecimal(range.maxValue.toString()).floatValue(); | ||
| break; | ||
| default: | ||
| return Optional.empty(); | ||
| } | ||
| return Optional.of(new float[] { min, max }); | ||
|
Check warning on line 578 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please return a Optional<Range<...>>. |
||
| } | ||
|
|
||
| private Double computeUniformRangeSelectivity(ColStatistics cs, Range<Float> boundaries, HiveTableScan scan, | ||
| boolean inverseBool, Range<Float> typeRange, RelDataType columnType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have the feeling that this method could be simplified by using the approach of computeTwoSidedUniformSelectivity ( |
||
| Optional<float[]> minMax = convertColRangeToFloatBounds(cs, columnType); | ||
| if (minMax.isEmpty()) { | ||
| return null; | ||
| } | ||
| float min = minMax.get()[0]; | ||
| float max = minMax.get()[1]; | ||
|
|
||
| float lowerInfinite = Float.NEGATIVE_INFINITY; | ||
| float upperInfinite = Float.POSITIVE_INFINITY; | ||
| boolean isOneSidedUpper = Float.compare(boundaries.lowerEndpoint(), lowerInfinite) == 0 | ||
| && Float.compare(boundaries.upperEndpoint(), upperInfinite) != 0; | ||
| boolean isOneSidedLower = Float.compare(boundaries.upperEndpoint(), upperInfinite) == 0 | ||
| && Float.compare(boundaries.lowerEndpoint(), lowerInfinite) != 0; | ||
|
|
||
| double rawSelectivity; | ||
| if (isOneSidedUpper) { | ||
| rawSelectivity = computeOneSidedUniformSelectivity(min, max, boundaries.upperEndpoint(), true, | ||
| BoundType.CLOSED.equals(boundaries.upperBoundType())); | ||
| } else if (isOneSidedLower) { | ||
| rawSelectivity = computeOneSidedUniformSelectivity(min, max, boundaries.lowerEndpoint(), false, | ||
| BoundType.CLOSED.equals(boundaries.lowerBoundType())); | ||
| } else { | ||
| rawSelectivity = computeTwoSidedUniformSelectivity(min, max, boundaries, inverseBool, typeRange); | ||
| } | ||
|
|
||
| if (rawSelectivity < 0 || Double.isNaN(rawSelectivity) || Double.isInfinite(rawSelectivity)) { | ||
| return null; | ||
| } | ||
| return scaleSelectivityForNulls(cs, Math.min(1.0, Math.max(0.0, rawSelectivity)), scan); | ||
|
Check warning on line 611 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| } | ||
|
|
||
| /** | ||
| * Mirrors {@code StatsRulesProcFactory.EvaluateComparatorWithRange} semantics for one-sided predicates. | ||
| */ | ||
| private static double computeOneSidedUniformSelectivity(float min, float max, float value, boolean upperBound, | ||
| boolean closedBound) { | ||
| Optional<Double> earlyReturn = applyOneSidedEarlyReturn(min, max, value, upperBound, closedBound); | ||
| if (earlyReturn.isPresent()) { | ||
| return earlyReturn.get(); | ||
| } | ||
| float domainWidth = max - min; | ||
| if (domainWidth <= 0) { | ||
| return 0; | ||
| } | ||
| if (upperBound) { | ||
| return (value - min) / domainWidth; | ||
| } | ||
| return (max - value) / domainWidth; | ||
| } | ||
|
|
||
| private static Optional<Double> applyOneSidedEarlyReturn(float min, float max, float value, boolean upperBound, | ||
|
Check failure on line 633 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| boolean closedBound) { | ||
| if (upperBound) { | ||
| if (max < value || (Float.compare(max, value) == 0 && closedBound)) { | ||
| return Optional.of(1.0); | ||
| } | ||
| if (min > value || (Float.compare(min, value) == 0 && !closedBound)) { | ||
| return Optional.of(0.0); | ||
| } | ||
| } else { | ||
| if (min > value || (Float.compare(min, value) == 0 && closedBound)) { | ||
| return Optional.of(1.0); | ||
| } | ||
| if (max < value || (Float.compare(max, value) == 0 && !closedBound)) { | ||
| return Optional.of(0.0); | ||
| } | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static double computeTwoSidedUniformSelectivity(float min, float max, Range<Float> boundaries, | ||
|
Check failure on line 653 in ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/FilterSelectivityEstimator.java
|
||
| boolean inverseBool, Range<Float> typeRange) { | ||
| if (Float.compare(min, max) == 0) { | ||
| double betweenSelectivity = isPointInClosedRange(boundaries, min) ? 1.0 : 0.0; | ||
| return inverseBool ? 1.0 - betweenSelectivity : betweenSelectivity; | ||
| } | ||
|
|
||
| Range<Float> domain = Range.closedOpen(min, Math.nextUp(max)); | ||
| Range<Float> predicateRange = convertRangeToClosedOpen(boundaries); | ||
|
|
||
| if (inverseBool) { | ||
| // when inverseBool == true, this is a NOT_BETWEEN and selectivity must be inverted | ||
| // if there's a cast, the inversion is with respect to its codomain (range of the values of the cast) | ||
| double typeRangeSelectivity = rangedSelectivity(kll, typeRange); | ||
| rawSelectivity = typeRangeSelectivity - rawSelectivity; | ||
| Range<Float> universe = domain; | ||
| if (typeRange != null) { | ||
| Range<Float> typeRangeClosedOpen = convertRangeToClosedOpen(typeRange); | ||
| universe = intersectClosedOpenRanges(domain, typeRangeClosedOpen); | ||
| if (universe == null) { | ||
| return 0; | ||
| } | ||
| } | ||
| float universeWidth = rangeWidth(universe); | ||
| if (universeWidth <= 0) { | ||
| return 0; | ||
| } | ||
| Range<Float> betweenIntersect = intersectClosedOpenRanges(universe, predicateRange); | ||
| float betweenWidth = betweenIntersect == null ? 0 : rangeWidth(betweenIntersect); | ||
| return 1.0 - betweenWidth / universeWidth; | ||
| } | ||
|
|
||
| Range<Float> intersect = intersectClosedOpenRanges(domain, predicateRange); | ||
| float overlapWidth = intersect == null ? 0 : rangeWidth(intersect); | ||
| float domainWidth = max - min; | ||
| if (domainWidth <= 0) { | ||
| return 0; | ||
| } | ||
| return overlapWidth / domainWidth; | ||
| } | ||
|
|
||
| private static boolean isPointInClosedRange(Range<Float> boundaries, float point) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use com.google.common.collect.Range#contains. |
||
| if (boundaries.isEmpty()) { | ||
| return false; | ||
| } | ||
| float lower = boundaries.lowerEndpoint(); | ||
| float upper = boundaries.upperEndpoint(); | ||
| boolean lowerOk = BoundType.CLOSED.equals(boundaries.lowerBoundType()) | ||
| ? Float.compare(point, lower) >= 0 | ||
| : Float.compare(point, lower) > 0; | ||
| boolean upperOk = BoundType.CLOSED.equals(boundaries.upperBoundType()) | ||
| ? Float.compare(point, upper) <= 0 | ||
| : Float.compare(point, upper) < 0; | ||
| return lowerOk && upperOk; | ||
| } | ||
|
|
||
| private static Range<Float> intersectClosedOpenRanges(Range<Float> left, Range<Float> right) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about calling this method "intersectRanges"? I don't see why it should be limited to closed-open ranges. |
||
| if (!left.isConnected(right)) { | ||
| return null; | ||
| } | ||
| Range<Float> intersection = left.intersection(right); | ||
| if (intersection.isEmpty()) { | ||
| return null; | ||
| } | ||
|
Comment on lines
+706
to
+712
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please return a valid range. If the Range is empty, then return |
||
| return intersection; | ||
| } | ||
|
|
||
| private static float rangeWidth(Range<Float> range) { | ||
| if (range == null || range.isEmpty()) { | ||
| return 0; | ||
| } | ||
| float width = range.upperEndpoint() - range.lowerEndpoint(); | ||
| return Math.max(width, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Adjust selectivity to account for NULL values, consistent with {@link #scaleSelectivityToNullableValues}. | ||
| * Unknown null count ({@code numNulls < 0}) is treated as zero nulls. | ||
| */ | ||
| private static double scaleSelectivityForNulls(ColStatistics cs, double rawSelectivity, HiveTableScan scan) { | ||
| if (scan.getTable() == null) { | ||
| return rawSelectivity; | ||
| } | ||
| double rowCount = scan.getTable().getRowCount(); | ||
| if (rowCount <= 0) { | ||
| return rawSelectivity; | ||
| } | ||
| long numNulls = cs.getNumNulls(); | ||
| if (numNulls < 0) { | ||
| numNulls = 0; | ||
| } | ||
| return scaleSelectivityToNullableValues(kll, rawSelectivity, scan); | ||
| double nonNullRows = Math.max(rowCount - numNulls, 0); | ||
| return nonNullRows * rawSelectivity / rowCount; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we combine these cases by using java.lang.Number#floatValue?
We could even change it to java.lang.Number#doubleValue. The method FilterSelectivityEstimator#extractLiteral(org.apache.calcite.rex.RexNode) returns float because the histogram stores float values. The range and boundary type could be changed to Double as well. Maybe this is out-of-scope for HIVE-29652, as it would require a bit of refactoring to not lose information.