Add a cluster config to rewrite exact DISTINCTCOUNT and PERCENTILE into approximations - #19510
Open
yashmayya wants to merge 1 commit into
Open
Add a cluster config to rewrite exact DISTINCTCOUNT and PERCENTILE into approximations#19510yashmayya wants to merge 1 commit into
yashmayya wants to merge 1 commit into
Conversation
…to approximations Exact DISTINCTCOUNT and PERCENTILE hold unbounded per-group state on servers, and numGroupsLimit caps the number of groups rather than the size of each group's accumulator. pinot.broker.use.approximate.function already rewrites them into the SMART variants, but it is single-stage only and is read once at broker startup. This makes it settable from the Helix cluster config with a live reload, adds tunable conversion thresholds, extends the rewrite to the multi-stage engine, and reports on the response when a query was rewritten. It also makes the SMART functions apply their threshold per group during group-by aggregation, without which the rewrite does not bound memory for the queries that need it most.
yashmayya
requested review from
Jackie-Jiang,
gortiz and
xiangfu0
and removed request for
Jackie-Jiang
September 8, 2026 19:03
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19510 +/- ##
============================================
+ Coverage 67.63% 67.76% +0.12%
Complexity 1430 1430
============================================
Files 3489 3491 +2
Lines 224427 224897 +470
Branches 35429 35525 +96
============================================
+ Hits 151797 152397 +600
+ Misses 60618 60457 -161
- Partials 12012 12043 +31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Exact
DISTINCTCOUNTandPERCENTILEhold unbounded per-group state on servers: aSetof every distinct value, or aDoubleArrayListof every raw value. Nothing caps them, becausenumGroupsLimitcaps the number of groups rather than the size of each group's accumulator. This is a recurring source of server and multi-stage worker OOMs.pinot.broker.use.approximate.functionalready rewrites these into the SMART variants, which stay exact until an accumulator crosses a threshold. Three things stop it being usable as a fleet-wide guard rail: it is read once in the broker constructor, so a cluster-config change needs a restart; the multi-stage engine ignores it entirely; and the conversion threshold cannot be set outside the SQL call itself.What this adds
Cluster config with a live reload. The existing key is now also read from the Helix cluster config, where it wins over the broker conf, and a change reaches a running broker through its ZooKeeper watch. Two new keys carry the conversion parameters, passed verbatim as the trailing argument of the rewritten call:
Worst-case memory for a group-by is
thresholdvalues per group, so the threshold wants sizing together withnumGroupsLimitrather than in isolation. The parameters are validated on the broker by parsing them with the aggregation function itself, so a typo is rejected there instead of failing every query on the servers.Off by default.
pinot.broker.use.approximate.functionstill defaults tofalse.Multi-stage support.
PinotApproximateAggregateRewriteRulerewritesDISTINCTCOUNT,DISTINCTCOUNTMV,COUNT(DISTINCT x),PERCENTILEandPERCENTILEMVonLogicalAggregate. It runs inPhase.BASIC, which it has to:PinotAggregateExchangeNodeInsertRulederives the leaf-to-final intermediate result format from the function name inPOST_LOGICAL, so a later rewrite, or one in the runtime, would desync the planned schema from the bytes the leaf produces. The rule pins the original return type, so switching the config on does not change the result schema.A per-query escape hatch. A new
useApproximateFunctionquery option overrides everything, in both directions. Precedence is query option > tableQueryConfig> cluster config > broker conf. The table level applies to the single-stage engine only, because a multi-stage query can span tables and so resolves the setting before it knows the table set.Reporting.
approximateFunctionAppliedon the broker response, alongsidenumGroupsLimitReachedandrlsFiltersApplied, plus anAPPROXIMATE_FUNCTION_OVERRIDESmeter and a query-log field. Trading exactness for memory should not be invisible.The group-by fix
Both SMART functions only converted at merge time.
percentileSmartTDigesthad no per-group check at all, anddistinctCountSmartHLLonly checked dictionary-encoded columns; their class javadocs said "For aggregation-only queries". Per-group accumulators therefore grew for a whole segment, so the rewrite would not have bounded memory for group-by queries, which is where these OOMs happen. Both now apply the threshold per accumulator.Behaviour note, unrelated to the new config: this changes results for existing callers of
distinctCountSmartHLL,distinctCountSmartHLLPlus,distinctCountSmartULLandpercentileSmartTDigestwithGROUP BY. Because a merged accumulator is a superset of each accumulator, a group that crosses the threshold in one segment would have crossed it at merge too, so the exact-or-approximate classification is unchanged wherever a merge happens; what changes is that the sketch is built per segment and then merged, rather than built once over the union. Where no merge happens at all, a single segment on a single server, an over-threshold group goes from exact to approximate, which is what the documented contract of these functions already said would happen.Two bugs found on the way, both needed here
AggregationFunctionType.getAggregationFunctionTyperejected every canonicalPERCENTILE*name that is not one of the deprecated numeric-suffix spellings, sopercentileSmartTDigestcould not be planned in the multi-stage engine at all. It now tries the canonical name first.That change would have silently masked a second bug, so it is fixed explicitly:
RAWKLLMVandRAWKLL<n>MVreturnedPERCENTILEKLLMV.percentileRawKLLMV(col)therefore changes from returning a DOUBLE to returning the VARCHAR sketch it was always meant to return.Separately,
DISTINCTCOUNTSMARTHLL,DISTINCTCOUNTSMARTHLLPLUSandDISTINCTCOUNTSMARTULLdeclared no final return type, so Calcite inferred BIGINT while the runtime returns INT. They now declareINTEGER, matchingDISTINCTCOUNT.Known caveat
In the single-stage engine an unaliased aggregation column is named after its function, so a rewritten
distinctcount(x)comes back asdistinctcountsmarthll(x). Values and column types are unchanged, the multi-stage engine keeps its Calcite-derived name, and an explicitASalias keeps the old one. This is how every broker-side function override already behaves, includingdistinctCountBitmapandsegmentPartitionedDistinctCount, and a test pins it.Testing
./mvnw testoverpinot-spi,pinot-segment-spi,pinot-common,pinot-core,pinot-query-plannerandpinot-brokerpasses.New tests cover the per-group conversion for both function families, including multi-valued columns, the BYTES branch, and values arriving after a group has converted; the config precedence, live reload and parameter validation; the planner rule for every rewritten spelling, for the functions it must leave alone, for both parameter strings in one query, and under the physical optimizer.
ApproximateFunctionOverrideIntegrationTestflips the cluster config on a running broker and checks both engines for the response flag, unchanged values and types,COUNT(DISTINCT), the query option, and a threshold low enough to prove the parameters reach the servers and parse there.