You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Store min/max of non-null values in column metadata #19781
Column minValue and maxValue in segment metadata include the placeholder that Pinot stores for null rows. With
null handling, this makes the values useless as bounds for a column that has nulls. Today each optimization that
reads min/max adds its own guard for nulls, and most guards simply disable the optimization.
This issue proposes to fix the cause. We add three optional, sparse keys per column to metadata.properties: numNulls, minNonNullValue and maxNonNullValue. The existing keys keep their meaning. Segments created before the
change report "unknown", and the optimizations keep their current conservative behavior for those segments. A
server-level flag lets operators backfill old segments on reload.
We want feedback on the format, the compatibility rules and the PR plan before we start.
Background
The forward index has no null representation. When a value is null, ingestion writes the defaultNullValue of the
column into the forward index and marks the doc in the null value vector. The stats collectors receive the
placeholder value, so it goes into the dictionary, minValue, maxValue and the cardinality.
For numeric dimensions the default is the minimum of the type (Integer.MIN_VALUE for INT). One null row is enough
to set minValue to that value. For metrics the default is 0, which can also become the min. A custom defaultNullValue does not solve the problem: any value can be an extreme in some segment.
The existing keys must keep this meaning, for three reasons:
When null handling is disabled in a query, a null row is the default value. MIN(col) from metadata or from the
dictionary must return it.
The bit-sliced range index uses the stored min as an offset. Its on-disk layout depends on it.
MinMaxValueBasedSelectionOrderByCombineOperator returned wrong rows (and an NPE) with null handling. The fix makes every segment with nulls unskippable when nulls sort first.
SelectionQuerySegmentPruner now prunes filtered ORDER BY col LIMIT n queries. It is disabled when null handling is active on the order-by or filter column, because nulls pollute min/max.
The streaming selection order-by combine prunes by min/max only when isNonNull flags the leading column. A segment with any null, or built before #19072, is always activated.
The non-scan MIN/MAX path of AggregationPlanNode is disabled when null handling is enabled and the segment has nulls.
With non-null bounds, #19724, #18692 and #19120 can prune segments that have nulls, instead of always processing
them.
Proposal
New keys
All keys are optional and live under column.<name>. in metadata.properties:
Key
When the writer writes it
numNulls
The column has one or more nulls, and the values below are representable.
minNonNullValue
Only when the min of the non-null values is different from minValue.
maxNonNullValue
Only when the max of the non-null values is different from maxValue.
The keys are sparse because tables can have hundreds of thousands of segments. A column without nulls gets no new
key. A column with nulls gets only numNulls when the default value is not an extreme, for example when defaultNullValue is between the real values.
The writer writes all the keys for a column, or none of them. It writes none when a value is not representable (for
example, a STRING longer than the 512-character metadata limit), or when the stored min/max is absent. "None" means
"unknown", which is always safe.
How a reader resolves the values
NullValueVectorCreator writes no bitmap file when a column has zero nulls. As a result, the presence of the bitmap
already means "one or more nulls", in old and new segments. No new marker is necessary:
Null bitmap
numNulls key
Result
absent
-
Known. Zero nulls, so the non-null min/max are minValue/maxValue.
present
present
Known. An absent minNonNullValue/maxNonNullValue means "equal to minValue/maxValue".
present
absent
Unknown. The segment is from an older version and has not been backfilled.
Java API
/// Statistics computed over the non-null values of a column in one segment./// `minValue` and `maxValue` are null only when every value is null.publicrecordNonNullStats(intnumNulls, intnumDocs,
@NullableComparable<?> minValue, @NullableComparable<?> maxValue) {
publicbooleanhasNulls() { returnnumNulls > 0; }
publicbooleanisAllNull() { returnnumNulls == numDocs; }
}
// DataSourceMetadata/// Returns null when the segment cannot supply the values.@NullabledefaultNonNullStatsgetNonNullStats() { returnnull; }
The immutable data source applies the resolution rules. The mutable data source tracks the values during
consumption, so it always returns them. Query code selects the variant from the query: with null handling it uses getNonNullStats(), and without null handling it uses getMinValue()/getMaxValue() as today.
Old segments
A reload does not change old segments by default. SegmentPreProcessor checks every segment when a server loads it,
so an always-on backfill would rewrite every old segment with nulls at server startup. A server-level
flag, disabled by default, enables the backfill.
The backfill is cheap in most cases. The forward index value at the first null doc is the default that was used when
the segment was created. If that value is not equal to minValue or maxValue, the stored values are already the
non-null bounds, and only numNulls (the bitmap cardinality) is written. A scan is necessary only for the extreme that
the default occupies. For a dictionary column with an inverted index, a bitmap operation replaces the scan.
The feature is only an optimization. Segments that stay "unknown" keep the current behavior, and retention removes
them over time.
Compatibility
There is no segment format version change. ColumnMetadataImpl ignores unknown keys, so an older server can load
a new segment, and a rollback is safe.
The new interface methods are default methods and return "unknown". External implementations of ColumnMetadata, ColumnStatistics and DataSourceMetadata still compile.
The new getters appear in the JSON of the server segment metadata REST API. This change only adds fields.
Planned PRs
Each PR keeps "unknown" as the fallback. Thus the PRs can merge in this order without a risk of wrong results.
SPI, reader, writer and offline creation. Add NonNullStats and the keys. Pass the null state of each row to the
stats collectors (row and columnar paths), which keep a running non-null min/max. Write the keys in BaseSegmentCreator, and apply the resolution rules in ImmutableDataSource.
Realtime and default columns. Track non-null min/max in MutableSegmentImpl, and pass them through the
realtime-to-offline converter statistics. Compacted segments compute them from valid docs only. Handle new default
columns and derived columns in BaseDefaultColumnHandler. Make sure that every path that rewrites column metadata
also rewrites or removes the new keys.
Backfill on reload, behind a server-level flag that is disabled by default.
Summary
Column
minValueandmaxValuein segment metadata include the placeholder that Pinot stores for null rows. Withnull handling, this makes the values useless as bounds for a column that has nulls. Today each optimization that
reads min/max adds its own guard for nulls, and most guards simply disable the optimization.
This issue proposes to fix the cause. We add three optional, sparse keys per column to
metadata.properties:numNulls,minNonNullValueandmaxNonNullValue. The existing keys keep their meaning. Segments created before thechange report "unknown", and the optimizations keep their current conservative behavior for those segments. A
server-level flag lets operators backfill old segments on reload.
We want feedback on the format, the compatibility rules and the PR plan before we start.
Background
The forward index has no null representation. When a value is null, ingestion writes the
defaultNullValueof thecolumn into the forward index and marks the doc in the null value vector. The stats collectors receive the
placeholder value, so it goes into the dictionary,
minValue,maxValueand the cardinality.For numeric dimensions the default is the minimum of the type (
Integer.MIN_VALUEfor INT). One null row is enoughto set
minValueto that value. For metrics the default is0, which can also become the min. A customdefaultNullValuedoes not solve the problem: any value can be an extreme in some segment.The existing keys must keep this meaning, for three reasons:
MIN(col)from metadata or from thedictionary must return it.
Work that this change would have helped
MinMaxValueBasedSelectionOrderByCombineOperatorreturned wrong rows (and an NPE) with null handling. The fix makes every segment with nulls unskippable when nulls sort first.SelectionQuerySegmentPrunernow prunes filteredORDER BY col LIMIT nqueries. It is disabled when null handling is active on the order-by or filter column, because nulls pollute min/max.isNonNullflags the leading column. A segment with any null, or built before #19072, is always activated.isNonNullcolumn flag. It covers only the zero-null case.MIN/MAXpath ofAggregationPlanNodeis disabled when null handling is enabled and the segment has nulls.With non-null bounds, #19724, #18692 and #19120 can prune segments that have nulls, instead of always processing
them.
Proposal
New keys
All keys are optional and live under
column.<name>.inmetadata.properties:numNullsminNonNullValueminValue.maxNonNullValuemaxValue.The keys are sparse because tables can have hundreds of thousands of segments. A column without nulls gets no new
key. A column with nulls gets only
numNullswhen the default value is not an extreme, for example whendefaultNullValueis between the real values.The writer writes all the keys for a column, or none of them. It writes none when a value is not representable (for
example, a STRING longer than the 512-character metadata limit), or when the stored min/max is absent. "None" means
"unknown", which is always safe.
How a reader resolves the values
NullValueVectorCreatorwrites no bitmap file when a column has zero nulls. As a result, the presence of the bitmapalready means "one or more nulls", in old and new segments. No new marker is necessary:
numNullskeyminValue/maxValue.minNonNullValue/maxNonNullValuemeans "equal tominValue/maxValue".Java API
The immutable data source applies the resolution rules. The mutable data source tracks the values during
consumption, so it always returns them. Query code selects the variant from the query: with null handling it uses
getNonNullStats(), and without null handling it usesgetMinValue()/getMaxValue()as today.Old segments
A reload does not change old segments by default.
SegmentPreProcessorchecks every segment when a server loads it,so an always-on backfill would rewrite every old segment with nulls at server startup. A server-level
flag, disabled by default, enables the backfill.
The backfill is cheap in most cases. The forward index value at the first null doc is the default that was used when
the segment was created. If that value is not equal to
minValueormaxValue, the stored values are already thenon-null bounds, and only
numNulls(the bitmap cardinality) is written. A scan is necessary only for the extreme thatthe default occupies. For a dictionary column with an inverted index, a bitmap operation replaces the scan.
The feature is only an optimization. Segments that stay "unknown" keep the current behavior, and retention removes
them over time.
Compatibility
ColumnMetadataImplignores unknown keys, so an older server can loada new segment, and a rollback is safe.
numNulls. The column then resolves to"unknown".
defaultmethods and return "unknown". External implementations ofColumnMetadata,ColumnStatisticsandDataSourceMetadatastill compile.Planned PRs
Each PR keeps "unknown" as the fallback. Thus the PRs can merge in this order without a risk of wrong results.
NonNullStatsand the keys. Pass the null state of each row to thestats collectors (row and columnar paths), which keep a running non-null min/max. Write the keys in
BaseSegmentCreator, and apply the resolution rules inImmutableDataSource.MutableSegmentImpl, and pass them through therealtime-to-offline converter statistics. Compacted segments compute them from valid docs only. Handle new default
columns and derived columns in
BaseDefaultColumnHandler. Make sure that every path that rewrites column metadataalso rewrites or removes the new keys.
MinMaxValueBasedSelectionOrderByCombineOperator. With null handling, use the non-null bounds, and treat a segmentas unskippable for nulls-first only when it has nulls. Keep the Fix wrong results from MinMax selection order-by combine under null handling #19724 behavior for "unknown" segments.
SelectionQuerySegmentPruner. Enable it with null handling when every segment involved supplies non-null stats.ColumnValueSegmentPruner. With null handling, EQ, IN and range predicates do not match null docs, so thepruner can use the non-null bounds.
Out of scope
startTimeandendTimein ZooKeeper. A null time value also changes them, but broker pruning on non-null boundswould break
WHERE ts IS NULL.MIN/MAXwith null handling. A future aggregation function for non-null min/max can use the new API.defaultNullValuein the metadata. The forward index value at the first null doc gives it.Questions for the community
numNullsmeans unknown" rule acceptable, or do you preferexplicit keys on every column?