[core] Optimize exact and TDigest percentile aggregation, including StarTree group-by#18996
[core] Optimize exact and TDigest percentile aggregation, including StarTree group-by#18996xiangfu0 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #18996 +/- ##
============================================
+ Coverage 65.21% 65.33% +0.11%
Complexity 1405 1405
============================================
Files 3418 3420 +2
Lines 214651 215425 +774
Branches 33922 34039 +117
============================================
+ Hits 139989 140747 +758
+ Misses 63356 63351 -5
- Partials 11306 11327 +21
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:
|
52f9ca2 to
4a950bd
Compare
4a950bd to
8cba43b
Compare
There was a problem hiding this comment.
Pull request overview
This PR optimizes Pinot percentile aggregations by replacing full sorts for exact percentiles with an in-place rank selection algorithm, and by substantially reducing TDigest aggregation overhead (raw ingestion, serialized merging, and star-tree query/group-by paths) via batched + linear merge strategies.
Changes:
- Replace exact-percentile full sorting with an introspective three-way rank selector (with bounded sort fallback).
- Introduce a
PercentileTDigestAccumulatorto efficiently merge raw values and serialized TDigest centroids (including star-tree and group-by paths) without repeated centroid materialization/sorting. - Add new deterministic JMH benchmarks and a percentile aggregation benchmark/user guide under
pinot-perf.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java | Adds coverage for TDigest value-aggregator sizing/serialization contracts and deferred compression behavior. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregator.java | Updates star-tree TDigest value aggregation to avoid per-update compression and to track safe serialized size bounds. |
| pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileTDigestValueAggregator.java | Adds JMH benchmark for the star-tree construction TDigest raw-value aggregation + serialization kernel. |
| pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileTDigestStarTreeAggregation.java | Adds JMH benchmark for merging serialized star-tree TDigest entries (non-grouped + group-by). |
| pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileTDigestAggregation.java | Adds end-to-end TDigest percentile aggregation benchmark over 100M rows. |
| pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileAggregation.java | Adds end-to-end and extraction-only exact percentile benchmarks over 100M rows. |
| pinot-perf/README.md | Links to the new percentile benchmark guide/results. |
| pinot-perf/benchmark-results/percentile-aggregation-100m-results.md | Adds user guide, sample queries/config, results, and reproduction commands for 100M-row percentile benchmarks. |
| pinot-core/src/test/java/org/apache/pinot/queries/PercentileTDigestQueriesTest.java | Extends query tests to validate star-tree TDigest GROUP BY correctness and fewer docs scanned vs non-star-tree. |
| pinot-core/src/test/java/org/apache/pinot/queries/PercentileTDigestMVQueriesTest.java | Disables the star-tree group-by test for the MV fixture (single-value star-tree stored metric). |
| pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java | Adds extensive correctness tests for raw + serialized TDigest aggregation and group-by behavior (including malformed inputs and lazy allocation). |
| pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunctionTest.java | Adds correctness tests to validate selection-based exact percentile extraction matches full-sort behavior across adversarial inputs. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java | Switches TDigest aggregation to use the new accumulator for raw and serialized (including group-by) paths. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java | Introduces a new internal accumulator to batch-sort raw values and linearly merge already-sorted serialized centroids. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunction.java | Replaces full sort in exact percentile final extraction with introselect-style selection + depth-limited fallback. |
xiangfu0
left a comment
There was a problem hiding this comment.
Found one high-signal TDigest serialization issue; see inline comment.
8cba43b to
633aab6
Compare
22329b9 to
b7b01bd
Compare
|
Reducer/group-by follow-up is now pushed in b7b01bd. Benchmark highlights (TDigest 3.2, 2 forks × 5 measurements):
The full report and reproduction commands are in pinot-perf/benchmark-results/percentile-aggregation-100m-results.md. Validation: 318 focused reducer/serde/concurrency tests passed; the 64-module warning-enabled test compilation passed (921 goals); spotless, checkstyle, license format/check, and diff-check all pass. New CI is running on this head. |
b7b01bd to
bb9600b
Compare
xiangfu0
left a comment
There was a problem hiding this comment.
Found one high-severity TDigest intermediate serialization issue; see inline comment.
bb9600b to
912280c
Compare
912280c to
25f1594
Compare
Summary
Why
Exact percentile previously sorted every retained value even though the query needs only one rank. TDigest 3.2
profiling found three additional hot spots:
byteSize()after every value, andbyteSize()compresses the digest;The exact path now uses three-way introselect with median/ninther pivoting and a bounded sort fallback. Raw and
serialized TDigest paths merge sorted primitive arrays directly. Star-tree construction maintains a non-compressing
serialized-size bound and compresses at serialization time. Oversized legacy compact digests retain compact encoding
so clone and forward-index readback remain valid with t-digest 3.2.
Intermediate results and distributed merging still use the standard
MergingDigestrepresentation.User guide and scope
The query optimizations are automatic. Exact percentile remains the choice for an exact order statistic:
Use TDigest for bounded, mergeable approximation:
The same stored metric is used for grouped queries:
A star-tree can pre-aggregate TDigest with the default compression used by that query:
{ "tableIndexConfig": { "starTreeIndexConfigs": [ { "dimensionsSplitOrder": ["region", "deviceType"], "skipStarNodeCreationForDimensions": [], "functionColumnPairs": ["percentileTDigest__latencyMs"], "maxLeafRecords": 10000 } ] } }An explicit query compression must match the index compression. Exact
PERCENTILEcannot use a stored star-treepercentile. The serialized query optimization covers non-grouped and group-by aggregation. Group states retain their
first serialized digest and allocate primitive merge buffers only when another entry reaches the group; the raw-value
buffer is allocated only if a raw value is added.
The complete user guide, workload boundaries, and commands are in
pinot-perf/benchmark-results/percentile-aggregation-100m-results.md.Benchmark results
Single-thread JMH 1.37 on an Apple M4 Pro with 24 GiB RAM and OpenJDK 21.0.10. Matched comparisons used two forks and
five measurements per fork.
The optimized star-tree query reduced allocation from 198,223,964 to 26,361 bytes/op and measured GC events from ten
to zero. It retained an exact digest size of 100 million and produced P75
0.7498860479, versus the sorted oracle0.7499315464(absolute error0.0000454985).The group-by extension uses
4a950bde9455—the preceding PR version with only the non-grouped optimization—as itsbaseline. Every workload merges 10,000 stored entries representing 100 million source rows and extracts every group:
The merge-heavy 100- and 1,000-group cases reduce allocation by 98.535% and 85.010%. At 10,000 groups each group has
only one digest, so there is no merge to optimize; confidence intervals overlap and allocation is effectively parity
(+0.703%).
That case keeps the first digest pending and avoids allocating unused raw/merge buffers.
The final optimized construction kernel also completed an actual 100-million-row run in 5,455.170 ms
(18.331 million rows/s). Its matched baseline comparison uses 1 million rows; no unmeasured 100-million-row baseline
is extrapolated. The construction benchmark includes per-group serialization but excludes dimension sorting and
forward-index I/O. The query benchmark excludes planner, traversal, forward-index I/O, and distributed latency.
Validation
PercentileTDigestAggregationFunctionTest: 41 tests passed, including serialized SV/MV group-by, lazy allocation,malformed input, and mixed serialized/raw growth coverage
PERCENTILETDIGEST ... GROUP BYexecution matcheduseStarTree=falseand scanned fewer documents;PercentileTDigestQueriesTestand its MV suite passed 12 testsPercentileTDigestValueAggregatorTest: 7 tests passed, including oversized compact clone/readbackspotless:apply: 0 files changed for the newly affectedpinot-coreandpinot-perfmodulescheckstyle:check: 0 violations in the newly affected moduleslicense:formatandlicense:check: passed in the newly affected modulestest-compilewith-Xlint:all: passed with no warnings on changed linespinot-segment-local-Xlint:allcompile: passed with no warnings on changed lines