Repository navigation
HIVE-29625: Disambiguate ColStatistics.countDistinct "unknown" from "verified zero" - #6505
konstantinb wants to merge 22 commits into
Conversation
|
@kasakrisz would you have time to review this one? You reviewed and merged #6423, and last month you merged HIVE-29365 in the stats-estimation area. The production change is +199/-100 across 8 files; the other 13 files are tests, and only two existing goldens change. @thomasrebele a first-pass review from you would also be very welcome, since this touches |
13d9437 to
f11d711
Compare
f11d711 to
12a3f8c
Compare
12a3f8c to
b988af0
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved statistics-estimation defects inflate multi-key group counts and mishandle all-NULL filters, producing incorrect cardinality estimates and potentially inappropriate aggregation choices.
3 open issues
What changed in this PR
This PR separates unknown distinct-value counts (-1) from verified zero in Hive’s statistics pipeline, allowing planner rules to apply different estimates for missing and empty domains.
Changes:
- Updates statistics producers, merging, and scaling to preserve unknown NDVs.
- Adjusts filter, join, grouping, and partition-planning consumers.
- Adds parameterized unit tests and join regressions, with two existing binary-column plan outputs updated.
| File | Description |
|---|---|
ql/src/test/results/clientpositive/llap/vector_binary_join_groupby.q.out |
Updates binary-key probe ratio. |
ql/src/test/results/clientpositive/llap/parquet_types_non_dictionary_encoding_vectorization.q.out |
Updates binary GROUP BY estimates. |
ql/src/test/results/clientpositive/llap/join_ndv_unknown_mixed.q.out |
Records mixed-NDV join estimates. |
ql/src/test/results/clientpositive/llap/join_ndv_unknown_mixed_multikey.q.out |
Records multi-key inner and outer join estimates. |
ql/src/test/queries/clientpositive/join_ndv_unknown_mixed.q |
Adds a mixed known/unknown join regression. |
ql/src/test/queries/clientpositive/join_ndv_unknown_mixed_multikey.q |
Adds correlated multi-key join regressions. |
ql/src/test/org/apache/hadoop/hive/ql/stats/TestStatsUtils.java |
Tests NDV production, merging, grouping, and scaling. |
ql/src/test/org/apache/hadoop/hive/ql/stats/estimator/TestPessimisticStatCombiner.java |
Tests unknown NDV propagation. |
ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestSortedDynPartitionOptimizer.java |
Tests partition-cardinality handling. |
ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestSetHashGroupByMinReduction.java |
Tests aggregation-reduction decisions. |
ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestReduceSinkMapJoinProc.java |
Tests hash-join key-count estimates. |
ql/src/test/org/apache/hadoop/hive/ql/optimizer/stats/annotation/TestStatsRulesProcFactory.java |
Migrates to JUnit 5 and expands statistics-rule coverage. |
ql/src/test/org/apache/hadoop/hive/ql/optimizer/calcite/stats/TestHiveRelMdDistinctRowCount.java |
Tests distinct-row metadata boundaries. |
ql/src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java |
Introduces unknown-aware producers and shared helpers. |
ql/src/java/org/apache/hadoop/hive/ql/stats/estimator/PessimisticStatCombiner.java |
Uses unknown-aware maximum merging. |
ql/src/java/org/apache/hadoop/hive/ql/plan/Statistics.java |
Preserves unknown NDVs during merges. |
ql/src/java/org/apache/hadoop/hive/ql/optimizer/stats/annotation/StatsRulesProcFactory.java |
Updates filter, grouping, and join estimates. |
ql/src/java/org/apache/hadoop/hive/ql/optimizer/SortedDynPartitionOptimizer.java |
Treats negative NDVs as unavailable statistics. |
ql/src/java/org/apache/hadoop/hive/ql/optimizer/SetHashGroupByMinReduction.java |
Preserves configured reduction for unknown NDVs. |
ql/src/java/org/apache/hadoop/hive/ql/optimizer/ReduceSinkMapJoinProc.java |
Separates unknown and zero key counts. |
ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/stats/HiveRelMdDistinctRowCount.java |
Prevents negative distinct-row metadata. |
Review effort: Balanced
| factor = 0; | ||
| break; |
There was a problem hiding this comment.
Keeping the clamp is intentional: hive.stats.filter.in.min.ratio is an opt-in floor for IN estimates without a usable value range, and a stored NDV of 0 is the kind of estimate it protects against. 17bccf8 rewrites the comment that overstated this and pins the interaction in TestStatsRulesProcFactory.
| if (dvs == 0) { | ||
| return 0; |
There was a problem hiding this comment.
Fixed in dcaac90 for the case this PR introduced: a verified-zero NDV means every row is NULL, so col <=> NULL (either operand order) now keeps all rows. Unknown and known NDVs keep master's estimates; using numNulls there too would change master's behavior, so that's left for a follow-up.
| } | ||
| ndv = parentNumRows / 2; | ||
| } | ||
| numEstimatedRows = StatsUtils.safeMult(numEstimatedRows, ndv); |
There was a problem hiding this comment.
Agreed: this changed master's decision, including for a single unknown key on a large input. Fixed in d69875a: an unknown NDV now keeps master's outcome (unknown was stored as 0, so the estimated hash table was empty), and the known-NDV product, which predates this PR, is unchanged.
thomasrebele
left a comment
There was a problem hiding this comment.
Thank you for the PR, @konstantinb! Sorry for the late reply, this has been on my TODO list for a long time.
This PR doesn't seem to change FilterStatsRule itself. It's quite related, so I had a first look. I had a look at the non-test files, and the changes that I understood looked reasonable (I need to take another deeper look at StatsRulesProcFactory). I wonder whether this PR covers all parts of the code that need to be changed. I would still need to check that.
| return numRows / 2; | ||
| } | ||
| if (dvs == 0) { | ||
| // no non-null values: only col <=> NULL can match, and then every row does |
There was a problem hiding this comment.
Could you explain this? What does <=> mean? "not equals"? If that's the case I would prefer using !=.
There was a problem hiding this comment.
Could you explain this? What does
<=>mean? "not equals"? If that's the case I would prefer using!=.
<=> is Hive's null-safe equality operator (IS NOT DISTINCT FROM), not inequality; I've clarified the comment while retaining the SQL notation. This branch addresses Copilot's earlier finding: with a verified-zero NDV, every row is NULL, so col <=> NULL must keep every row.
|




Reviewer summary: the production change is +200/-102 across 8 files; the other 17 files
(+2,216/-211) are tests. Only two existing
.q.outgoldens change, both because binary columns,whose metastore stats carry no NDV, now report an unknown NDV (
-1) instead of an implicit0that consumers took as a real value. Suggested reading order:
StatsUtils, thenStatisticsand
PessimisticStatCombiner, thenStatsRulesProcFactory.What changes were proposed in this pull request?
HIVE-29625: Disambiguate
ColStatistics.countDistinct"unknown" from "verified zero".Establishes
-1as the "unknown" NDV sentinel forColStatistics.countDistinct, parallel tothe historical convention for
numNulls/numTrues/numFalses, which HIVE-29438 reinforced.Producers emit
-1when the NDV is genuinely unavailable:ColStatisticsconversion returns-1whennumDVsis unset (previously thethrift default
0);explicitly report
-1(tsltz likewise fornumNulls).Consumers of
getCountDistint()distinguish< 0(unknown),== 0(verified zero) and> 0(verified positive). Unknown triggers the same fallback the overloaded0used totrigger; verified zero flows through the math naturally. Two consumer rules are worth calling
out:
getDenominator) compute on the known values whenonly some inputs are unknown — the known NDVs bound the join-key domain from below, so this
reproduces the previous behavior (an unknown-as-
0lost the max for two relations and wasthe excluded least value for three or more) instead of collapsing to a cross-product
estimate. Only an all-unknown input makes the denominator itself unknown.
getDenominatorForUnmatchedRows) propagate unknown: an unknown mayresolve below every known value, so no known value is a safe bound.
Two small shared helpers consolidate previously duplicated logic:
StatsUtils.maxOrUnknown(five call sites:Statistics.addToColumnStatsplus all four fieldmerges in
PessimisticStatCombiner) andStatsUtils.scaleDownNDV(three call sites; alsounifies the row-count-ratio boundary at
< 1.0and keeps NDVs above 2^53 exact by skippingthe double round-trip when the row count does not shrink).
This also fixes HIVE-29556 (
extractNDVGroupingColumnsadjusting an unknown NDV of0to1).Why are the changes needed?
ColStatistics.countDistinct == 0was overloaded to mean both "no distinct values" and"unknown NDV". The two states have opposite implications for cost-based planning — a real zero
supports tight estimates, while a genuinely-unknown NDV needs a conservative fallback. Treating
them identically led to inconsistent and sometimes severe cardinality estimates whenever a
column's NDV was unavailable: for example, a join between a table with a binary key (NDV always
unavailable) and a
castexpression with a known NDV could only be estimated as if no keystatistics existed at all. Disambiguating the sentinel lets each consumer apply the correct
logic.
@zabetak noted during review of #6359: "In fact, everything would be simpler if we could use -1
for NDV to declare unknown as it happens for the other stats. This is probably a bigger change to
digest so let's not go into this direction for now."
(#6359 (comment)) This PR takes up that bigger
change.
Does this PR introduce any user-facing change?
No. Query results, SQL syntax, and configuration are unchanged. Plan estimates in EXPLAIN
output may differ for queries reading columns whose NDV is unavailable, since the planner now
distinguishes those from columns with
0distinct values.How was this patch tested?
(
TestStatsUtils,TestStatsRulesProcFactory,TestPessimisticStatCombiner, plus the newoptimizer tests under
ql/src/test/.../optimizer/), covering unknown / verified-zero /verified-positive for each site and the row-count-ratio boundary (including NDVs above 2^53).
.qregression tests:join_ndv_unknown_mixed.q(mixed known/unknown join-keyNDVs),
join_ndv_unknown_mixed_multikey.q(correlated multi-key inner and left-outer joins),groupby_ndv_unknown_multikey.q(preserving master's hash-aggregation sizing decision forunknown NDVs), and
filter_ndv_verified_zero_null_safe.q(null-safe equality against NULLwith a verified-zero NDV).
.q.outexpected-result files: only two shift, both traced to the binary NDVproducer change — the
probeDecodeDetailsline invector_binary_join_groupby.qand thebinary GROUP BY estimates in
parquet_types_non_dictionary_encoding_vectorization.q. Allaffected test classes pass.
TestStatsRulesProcFactorynow uses JUnit 5 so it can host the parameterized cases. As part ofthat, its 16 existing min/max comparison tests were folded into four parameterized groups, with
every case's operator, literal and expected row count preserved.