Skip to content

HIVE-29652:Improve range selectivity estimations in the absence of histograms - #6746

Open
Manya0407 wants to merge 1 commit into
apache:masterfrom
Manya0407:CBO
Open

HIVE-29652:Improve range selectivity estimations in the absence of histograms#6746
Manya0407 wants to merge 1 commit into
apache:masterfrom
Manya0407:CBO

Conversation

@Manya0407

@Manya0407 Manya0407 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Adds MIN/MAX uniform-distribution selectivity estimation to FilterSelectivityEstimator when histograms are absent. The hierarchy is now: histogram → MIN/MAX (if hive.stats.filter.range.uniform=true) → existing 1/3 / 1/NDV fallback. Also wires the config through HiveConfPlannerContext/CalcitePlanner and adds unit tests including DATE days→seconds handling.

Why are the changes needed?

Without histograms, CBO currently uses hardcoded selectivity (1/3, 1/NDV) even when MIN/MAX stats exist, hurting cardinality estimates and plan quality. The annotation path already uses MIN/MAX uniform estimation; this aligns the CBO path with that behavior.

Does this PR introduce any user-facing change?

No

How was this patch tested?

Added unit tests in TestFilterSelectivityEstimator for comparisons, BETWEEN/NOT BETWEEN, boundaries, nulls, config off, SEARCH, and DATE conversion. Ran mvn -pl ql -Dtest=TestFilterSelectivityEstimator test

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

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

Thank you for the PR, @Manya0407! I've left some suggestions how the code could be simplified. There are also some unexpected test results to be discussed.

Comment on lines +543 to +570
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;
case FLOAT:
min = range.minValue.floatValue();
max = range.maxValue.floatValue();
break;
case DOUBLE:
min = (float) range.minValue.doubleValue();
max = (float) range.maxValue.doubleValue();
break;

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.

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.

return (RexLiteral) REX_BUILDER.makeLiteral(calendar,
REX_BUILDER.getTypeFactory().createSqlType(SqlTypeName.DATE), true);
}

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.

I think we could use org.apache.calcite.rex.RexBuilder#makeDateLiteral here.

default:
return Optional.empty();
}
return Optional.of(new float[] { min, max });

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.

Please return a Optional<Range<...>>.

Comment on lines +706 to +712
if (!left.isConnected(right)) {
return null;
}
Range<Float> intersection = left.intersection(right);
if (intersection.isEmpty()) {
return null;
}

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.

Please return a valid range. If the Range is empty, then return Range.closedOpen(0f, 0f). The callers that check for null can then be simplified.

return lowerOk && upperOk;
}

private static Range<Float> intersectClosedOpenRanges(Range<Float> left, Range<Float> right) {

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.

How about calling this method "intersectRanges"? I don't see why it should be limited to closed-open ranges.

return overlapWidth / domainWidth;
}

private static boolean isPointInClosedRange(Range<Float> boundaries, float point) {

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.

Please use com.google.common.collect.Range#contains.

}

private Double computeUniformRangeSelectivity(ColStatistics cs, Range<Float> boundaries, HiveTableScan scan,
boolean inverseBool, Range<Float> typeRange, RelDataType columnType) {

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.

I have the feeling that this method could be simplified by using the approach of computeTwoSidedUniformSelectivity (intersect(intersect(minMaxRange, typeRange), boundaries)) also for one-sided predicates. Could you try that, please?

RexNode int50 = REX_BUILDER.makeLiteral(50, TYPE_FACTORY.createSqlType(INTEGER), true);
RexNode filter = REX_BUILDER.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, currentInputRef, int50);
FilterSelectivityEstimator estimator = new FilterSelectivityEstimator(scan, mq);
Assert.assertEquals(0.5, estimator.estimateSelectivity(filter), DELTA);

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.

Shouldn't the expected result be 51/101? There are 101 integers in the interval [0, 100] and 51 integers from that interval that fulfill the predicate x <= 50, i.e., [0, 50]. So I would have expected the selectivity to be 51/101 or about 0.5049505.

RexNode filter = REX_BUILDER.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, currentInputRef,
literalDate("2020-11-04"));
FilterSelectivityEstimator estimator = new FilterSelectivityEstimator(scan, mq);
Assert.assertEquals(0.5, estimator.estimateSelectivity(filter), DELTA);

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.

Similar to https://github.com/apache/hive/pull/6746/changes#r3903821835, I would have expected a selectivity of 4/7 or 0.5714286.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants