Skip to content

Add opt-in non-blocking group-by combine operator#14698

Closed
xiangfu0 wants to merge 7 commits into
apache:masterfrom
xiangfu0:groupby-optimization
Closed

Add opt-in non-blocking group-by combine operator#14698
xiangfu0 wants to merge 7 commits into
apache:masterfrom
xiangfu0:groupby-optimization

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Dec 22, 2024

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in non-blocking server-side GROUP BY combine operator. Each worker accumulates into an exclusively owned
SimpleIndexedTable, then the workers combine their tables through an atomic tournament that merges the smaller table
into the larger one.

The existing CONCURRENT implementation remains the server default and rollout gate. The non-blocking path is used
only for general GROUP BY queries with ORDER BY after an operator enables it in server configuration. Sorted
safe-trim operators still take precedence, and GROUP BY queries without ORDER BY use the shared concurrent table.

The experimental partitioned implementation was removed after review found correctness and peak-memory risks that
were not justified by its additional complexity.

Motivation

GroupByCombineOperator sends every worker's upserts through one shared ConcurrentIndexedTable. High-cardinality
queries spend substantial time contending on that table. The non-blocking implementation moves the hot accumulation
loop to ordinary per-worker HashMaps and performs a bounded reduction after segment processing.

Design

  • Build one thread-confined SimpleIndexedTable per active combine worker.
  • Exchange completed tables through an AtomicReference without a central merge lock.
  • Merge smaller tables into larger tables to reduce repeated hash-map growth.
  • Transfer existing Key and Record ownership while destructively draining source maps, reducing allocation and
    peak retained memory during reduction.
  • Stop partially drained tournament merges after a worker failure and discard both exclusively owned tables.
  • Avoid capturing map-update lambdas in the simple-table hot path.
  • Reuse TableResizer candidate storage instead of allocating one wrapper per examined record.
  • Preserve cancellation checks and aggregate trim/resize statistics across table merges.
  • Preserve the legacy unordered-table insertion behavior used by downstream reducers.
  • Keep CONCURRENT as the default because per-worker tables can require more peak memory.
  • Prevent a query option from bypassing the server rollout gate.
  • Route no-ORDER BY GROUP BY queries to CONCURRENT; worker-local LIMIT admission can otherwise discard partial
    aggregates before the global merge.

Rollout and usage

Enable the algorithm on a server after validating representative query cardinality, execution-thread count, and
memory headroom:

pinot.server.query.executor.group.by.algorithm=NON-BLOCKING

With that server gate enabled, a query can explicitly select the optimized path:

SET groupByAlgorithm = 'NON-BLOCKING';
SELECT d1, d2, SUM(m1), MAX(m2)
FROM testTable
GROUP BY d1, d2
ORDER BY SUM(m1) DESC
LIMIT 500;

A query can fall back to the shared implementation without changing server configuration:

SET groupByAlgorithm = 'CONCURRENT';
SELECT d1, d2, SUM(m1), MAX(m2)
FROM testTable
GROUP BY d1, d2
ORDER BY SUM(m1) DESC
LIMIT 500;

If the server remains configured as CONCURRENT, a query cannot opt itself into NON-BLOCKING.

Performance

JMH on JDK 25, 8 combine threads, 4 forks, and 3 measured iterations per fork. The harness explicitly rejects unknown
algorithm names and was invoked with -p '_algorithm=CONCURRENT,NON-BLOCKING'. The benchmark query is:

SELECT d1, d2, SUM(m1), MAX(m2)
FROM testTable
GROUP BY d1, d2
ORDER BY SUM(m1) DESC
LIMIT 500
Workload CONCURRENT NON-BLOCKING Speedup Allocation reduction
100 segments x 100K records, repeated 10-dataset pool 4,124.7 ms/op 1,689.6 ms/op 2.44x 20.8%
10 segments x 1M records, 10 distinct datasets 6,153.2 ms/op 1,562.8 ms/op 3.94x 22.5%
100 segments x 100K records, 100 distinct datasets (~6.32M groups) 6,184.0 ms/op 1,386.7 ms/op 4.46x 26.4%

Normalized allocation:

Workload CONCURRENT NON-BLOCKING
100 x 100K, 10-dataset pool 2,715,889,806 B/op 2,151,570,302 B/op
10 x 1M, 10 datasets 2,886,638,027 B/op 2,236,964,905 B/op
100 x 100K, 100 datasets 2,925,703,405 B/op 2,154,772,481 B/op

Validation

Targeted correctness coverage includes legacy and non-blocking inter-segment queries, trim behavior, cancellation,
rollout/default selection, a forced four-worker CAS tournament, deterministic partial-merge failure, no-ORDER BY
partial aggregates, and the gapfill regressions found by the JDK 25 CI shard.

TZ=UTC ./mvnw -pl pinot-core \
  -Dtest=GapfillQueriesTest,GapfillQueriesScalabilityTest,NonblockingGroupByCombineOperatorTest,CombinePlanNodeTest,IndexedTableTest,DeterministicIndexedTableTest,TableResizerTest,SortedGroupByCombineOperatorsTest,InstancePlanMakerImplV2Test,InterSegmentGroupBySingleValueQueriesTest,InterSegmentNonBlockingGroupBySingleValueQueriesTest \
  test

Result on Temurin 25: 247 tests passed.

Required module checks also pass:

./mvnw spotless:apply -pl pinot-common,pinot-core,pinot-spi,pinot-perf
./mvnw checkstyle:check -pl pinot-common,pinot-core,pinot-spi,pinot-perf
./mvnw license:format -pl pinot-common,pinot-core,pinot-spi,pinot-perf
./mvnw license:check -pl pinot-common,pinot-core,pinot-spi,pinot-perf

CI

All 11/11 required GitHub Actions checks passed on exact head bb9a6b212a2f63b607272a69a78c8f7ade5be7ad, including both unit shards, both integration shards, quickstart, linter, binary compatibility, and all four compatibility regressions.

@xiangfu0
xiangfu0 marked this pull request as draft December 22, 2024 09:35
@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch 2 times, most recently from 7fe387a to 0dd717f Compare December 22, 2024 09:50
@codecov-commenter

codecov-commenter commented Dec 22, 2024

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.76923% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.42%. Comparing base (2298c6d) to head (bb9a6b2).

Files with missing lines Patch % Lines
.../core/operator/combine/GroupByCombineOperator.java 76.56% 11 Missing and 4 partials ⚠️
...tor/combine/NonblockingGroupByCombineOperator.java 70.58% 6 Missing and 4 partials ⚠️
...org/apache/pinot/core/data/table/IndexedTable.java 77.27% 1 Missing and 4 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #14698      +/-   ##
============================================
- Coverage     65.43%   65.42%   -0.02%     
  Complexity     1421     1421              
============================================
  Files          3425     3426       +1     
  Lines        216151   216254     +103     
  Branches      34231    34246      +15     
============================================
+ Hits         141436   141480      +44     
- Misses        63337    63376      +39     
- Partials      11378    11398      +20     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 65.42% <80.76%> (-0.02%) ⬇️
temurin 65.42% <80.76%> (-0.02%) ⬇️
unittests 65.42% <80.76%> (-0.02%) ⬇️
unittests1 56.86% <80.76%> (-0.01%) ⬇️
unittests2 37.77% <1.92%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch 3 times, most recently from caa59d4 to 5eee7a3 Compare December 22, 2024 11:55
@ankitsultana

Copy link
Copy Markdown
Contributor

@xiangfu0 : are you folks also planning to run some benchmarks? And any other ideas you are already trying out?

@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch 16 times, most recently from 530b877 to 758d7fe Compare December 25, 2024 12:16
@wirybeaver

Copy link
Copy Markdown
Contributor

@xiangfu0 Glad to see the master is working on the partitioned groupBy algorithm. Do we plan to support disk spilled mode? #12080

@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch from 758d7fe to fc14886 Compare March 27, 2026 02:32
@xiangfu0 xiangfu0 changed the title Add a non-blocking groupBy implementation Add non-blocking and partitioned groupBy implementations Mar 27, 2026
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Updated with commit fc14886. The partitioned combine path now uses thread-local per-partition tables plus parallel final reduction, which removes the 8-thread initialization race and improves the partitioned path on the same local synthetic workload from 106.341 ms to 77.933 ms at 4 threads (~27% faster). On 8 threads the previous implementation reproduced the NPE; the current code completes at 59.218 ms, and in the current 3-way comparison the partitioned path is 54.288 ms vs 68.945 ms for non-blocking and 150.688 ms for default.

@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch from fc14886 to 2dc4573 Compare March 27, 2026 06:31
@xiangfu0
xiangfu0 requested a review from Copilot March 27, 2026 06:32
@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch from 7aba9ff to 57771a1 Compare March 31, 2026 01:41
@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch 4 times, most recently from 69f5945 to 70273e1 Compare April 8, 2026 01:57
@xiangfu0
xiangfu0 requested a review from Copilot April 8, 2026 08:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Comment thread pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java Outdated
@xiangfu0
xiangfu0 force-pushed the groupby-optimization branch 8 times, most recently from 78bf1ad to d5414e3 Compare April 9, 2026 10:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Comment thread pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java Outdated
@xiangfu0 xiangfu0 changed the title Partitioned group-by combine with per-partition thread-local tables Replace default group-by combine with non-blocking per-thread operator Apr 9, 2026
@xiangfu0 xiangfu0 changed the title Replace default group-by combine with non-blocking per-thread operator Replace default group-by combine with non-blocking per-thread operator (CONCURRENT fallback) Apr 9, 2026
xiangfu0 and others added 7 commits July 23, 2026 01:57
Introduce two new server-level group-by combine algorithms selectable
via the `groupByAlgorithm` query option:

- NONBLOCKING: Uses a lock-free ConcurrentHashMap to aggregate results
  across segments without holding a global lock, reducing contention
  under high concurrency.

- PARTITIONED: Hash-partitions group keys across independent tables so
  that each partition can be aggregated and trimmed in parallel with
  zero cross-partition synchronization. Includes per-partition
  thread-local accumulation, cached key hashing, and parallel
  finish/trim to minimize overhead for high-cardinality queries.

Also adds:
- `normalizeGroupByAlgorithm` helper in QueryOptionsUtils
- `IndexedTable.transferFrom` for bulk partition merging
- JMH benchmark for comparing combine operator throughput
- Unit and integration tests for both new algorithms
…tests

- GroupByCombineOperator.ALGORITHM: "DEFAULT" → "CONCURRENT" (descriptive name
  for the shared ConcurrentHashMap-based operator; "DEFAULT" was misleading since
  it is no longer the default)
- CombinePlanNode: update comment to reference CONCURRENT
- InterSegmentNonBlockingGroupBySingleValueQueriesTest: use
  QueryOptionKey.GROUP_BY_ALGORITHM constant instead of raw string, use
  NonblockingGroupByCombineOperator.ALGORITHM / GroupByCombineOperator.ALGORITHM
  constants instead of hardcoded strings, add tests for the CONCURRENT fallback path
- Add Server.GROUP_BY_ALGORITHM config key so operators can set a fleet-wide
  default algorithm without modifying individual queries (MAJOR: no server-level
  rollback path). InstancePlanMakerImplV2 reads it at init and falls back to
  NON-BLOCKING. Per-query SET groupByAlgorithm=... still overrides.

- Revert GroupByCombineOperator fields back to private (only _indexedTable was
  needed by the subclass). Replace direct field access with three protected
  exchange methods: stealSharedTable(), depositSharedTable(), hasSharedTable().
  NonblockingGroupByCombineOperator now uses these methods instead of reaching
  into the parent's internal state (MINOR: overly wide protected field scope).

- Make EXPLAIN_NAME protected in GroupByCombineOperator; remove the duplicate
  constant and the redundant toExplainString() override from the subclass
  (MINOR: duplicate constant).

- Add Javadoc to IndexedTable.iterator() documenting the pre-finish semantics
  and to absorbTrimStats() documenting the non-thread-safe ownership requirement
  (MINOR: undocumented contract change).
…ment accuracy

- Replace h^h>>>16 secondary hash with Knuth multiplicative hash (0x9E3779B9) in
  partitionFor() to eliminate HashMap bucket clustering in partition-local tables
- Fix trim storm in maybeTrimLocalPartitions: loop until actualTotal drops below
  threshold instead of trimming only the largest partition once
- Clarify _indexedTable comment to accurately reflect volatile DCL access pattern
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Related to performance optimization query Related to query processing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants