xiangfu0 commented on PR #19264:
URL: https://github.com/apache/pinot/pull/19264#issuecomment-5518549451

   ## Design rationale (for reviewers)
   
   This summarizes the design decisions and their rationale, so the diff is 
easier to follow. The commit history shows some evolution (an intermediate 
cardinality gate that was later removed); this describes the **final** design.
   
   ### Problem
   
   `GROUP BY GROUPING SETS / ROLLUP / CUBE` expanded every input row into one 
group per grouping set **in each segment**, so per-set fan-out scaled with 
scanned rows (`O(rows × numSets)`) and single-threaded per segment. ROLLUP/CUBE 
were several times slower than a plain GROUP BY over the same columns.
   
   ### Core idea: base aggregation + merge-time derive
   
   1. **Segment phase** aggregates only the *base grouping* (the union of all 
grouping-set columns), exactly like a plain GROUP BY — one group per row, no 
`$groupingId`. It emits the base groups.
   2. **Server combine** (`GroupByCombineOperator`) merges base groups across 
segments, then **derives** the individual grouping sets once, in 
`mergeResults()`, **parallelized across the combine thread pool** by base-group 
ranges into a shared concurrent table.
   
   Total work is minimal (each row grouped once, never per set), and the 
per-set fan-out is multi-threaded and runs *after* the row-collapsing base 
merge.
   
   **Why it is exact for every aggregation.** Deriving a coarser set = merging 
the base groups' *intermediate* results. That is exactly 
`AggregationFunction#merge`, which Pinot already requires for scatter-gather 
(segment→server→broker). So every mergeable aggregation is correct by 
construction — no per-function special-casing.
   
   **Object-accumulator safety.** A base group's intermediate flows into every 
grouping set (and across threads), and `merge` mutates/returns its argument. So 
each base intermediate is **cloned per derived record** (serialize/deserialize 
round-trip; scalar intermediates are immutable and skipped). This keeps AVG / 
DISTINCTCOUNT / percentiles exact under concurrent cross-set merges.
   
   ### Trimming: accuracy vs. cost, made configurable
   
   The derive is a bounded transformation of the already-bounded base groups 
(base ≤ `numGroupsLimit` per segment ⇒ derived ≤ `numGroupsLimit × numSets`), 
so it must **not** re-apply the per-segment `numGroupsLimit` cap — doing so 
dropped derived groups **non-deterministically** under parallel upsert and 
could starve a low-magnitude set (e.g. the grand total). The derive keeps all 
groups by default and defers ORDER BY + LIMIT to the broker.
   
   For high-cardinality unions where keeping everything is expensive, a new 
**`groupingSetsServerTrimSize`** option applies a **per-set** (bucketed on 
`$groupingId`) top-K on the server after the derive. Per-set bucketing is the 
safe kind of trim: it never starves a set, only ranks within each. Default is 
exact (defer to broker); set it for a bounded, approximate per-server top-K 
(broker still does the exact final trim).
   
   ### Carve-outs (fall back to per-row expansion)
   
   Only the standard `GroupByCombineOperator` performs the merge-time derive. 
Base aggregation is disabled — segments expand per row and emit `$groupingId` 
directly — when a different combine path runs:
   - **Multi-value group-by columns** — an MV column fans a row across its 
values in the base grouping, so rolling it up would over-count.
   - **Filtered aggregations** — share a single group-key generator across 
aggregation groups (distinct segment path).
   - **Streaming combine** (MSE leaf stages, `streamingGroupByFlushThreshold > 
0`) — flushes incrementally and cannot derive over the fully-merged base table.
   - **Sorted-aggregate combine** — has its own merge that does not derive 
(defensive: grouping-set queries always take the unsafe-trim path, so this is 
not normally reached).
   
   ### Enablement
   
   Enabled by default via `groupingSetsBaseAggregation`; set to `false` to 
force the legacy per-row expansion path. Results are identical to expansion 
(verified by equivalence tests). Open for reviewer discussion: whether a 
behavior-changing computation path should default ON or OFF-until-validated.
   
   ### Supporting fix
   
   `IndexedTable` now derives its key-column count from the schema (`columns − 
aggregations`) rather than always from `getNumGroupByKeyColumns()`. The base 
combine table's records omit the `$groupingId` column, so the old fixed count 
merged aggregations at the wrong offset (dropping the first aggregation's 
cross-segment contribution / reading out of bounds when a base key repeated 
across segments). This is exercised by a new multi-segment test.
   
   ### Legacy-generator improvements (fallback path)
   
   For the carve-out cases and `groupingSetsBaseAggregation=false`, 
`GroupingSetsGroupKeyGenerator` now: resolves dict-encoded columns via native 
dict-ids (no re-hash), packs composite keys into a `long` + 
`Long2IntOpenHashMap` when they fit (no per-group object/array hashing), and 
reuses per-row group-id buffers across blocks.
   
   ### Tests
   
   - `GroupingSetsQueriesTest` — base-agg vs. expansion equivalence across 
DISTINCTCOUNT/AVG/SUM over CUBE/ROLLUP/GROUPING SETS, with/without null 
handling; server-trim bounds output without starving sets; existing 
ROLLUP/CUBE/GROUPING()/null/MV cases.
   - `DeriveGroupingSetsTest` — parallel derive identical across thread counts 
{1,2,4,8}; cross-segment merge (reproduces the `IndexedTable` bug without the 
fix); no group dropped / deterministic even when derived groups exceed a small 
`numGroupsLimit`.
   - `StreamingGroupByCombineOperatorTest` — grouping-set streaming path emits 
the correct `$groupingId` layout.
   
   ### Benchmark
   
   `BenchmarkGroupingSetsQueriesSSE` (full server→broker, 50 segments × 15k 
rows, 2 forks × 5 iters): base aggregation is **1.8–2.7× faster** at 
low/moderate cardinality (many rows collapse into few base groups) and roughly 
**parity** at extreme cardinality (base groups ≈ rows, so little to collapse). 
Numbers in the PR description.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to