Skip to content

Add streaming selection ORDER BY combine for physically sorted segments - #19120

Draft
rohityadav1993 wants to merge 2 commits into
apache:masterfrom
rohityadav1993:oss/pr1-streaming-selection-combine
Draft

Add streaming selection ORDER BY combine for physically sorted segments#19120
rohityadav1993 wants to merge 2 commits into
apache:masterfrom
rohityadav1993:oss/pr1-streaming-selection-combine

Conversation

@rohityadav1993

Copy link
Copy Markdown
Contributor

An unbounded leaf-stage ORDER BY (the shape injected below a sorted merge join input, and also
reachable directly) routes to MinMaxValueBasedSelectionOrderByCombineOperator, which merges every
segment's rows into a single materialized block before returning anything. At large data volumes
this exceeds the leaf stage's CPU budget and ThreadAccountant raises EarlyTerminationException
inside SelectionOperatorUtils.mergeWithOrdering(), surfacing at the broker as a spurious
Cancelled by sender. This is the first bullet of Challenge 1 in #18667.

This PR adds a streaming alternative for segments that are physically sorted on the leading
ORDER BY column.

Approach

  • StreamingSelectionOrderByOperator (new) emits sorted blocks incrementally for a segment that
    is physically sorted on the leading ORDER BY column, walking the sorted forward index in order
    instead of filling a priority queue with the whole segment. Multi-column ORDER BY is handled by
    a second pass over each equal-prefix run.
  • StreamingSelectionOrderByCombineOperator (new) performs a k-way heap merge across the
    per-segment operators and emits bounded blocks rather than one materialized result. Segments are
    acquired and released incrementally, and the merge loop performs periodic termination /
    deadline / resource-usage sampling, matching BaseStreamingCombineOperator.
  • SelectionPlanNode, CombinePlanNode, InstancePlanMakerImplV2 and QueryContext
    select these operators only when the option is set and the sortedness precondition holds; any
    unmet precondition falls back to the existing operators.
  • A data-schema mismatch between segments (possible mid-reload) is reported as a processing
    exception rather than merged blindly, mirroring SelectionOrderByResultsBlockMerger.

Opt-in query options

Option Default Meaning
sortedSelectionMergeEnabled false Use the streaming selection ORDER BY combine when the sortedness precondition holds
sortedSelectionMergeBlockSize 10000 Rows per emitted block (Broker.DEFAULT_SORTED_SELECTION_MERGE_BLOCK_SIZE)

No behaviour change when the option is off

Without sortedSelectionMergeEnabled, planning and execution take the existing path unchanged — the
new operators are never constructed. No existing option, plan node, or wire format changes.

Tests

64 tests, all green:

Test class Count
StreamingSelectionOrderByOperatorTest (new) 13
StreamingSelectionOrderByCombineOperatorTest (new) 15
CombineSlowOperatorsTest (extended) 11
QueryOptionsUtilsTest (extended) 25

CombineSlowOperatorsTest.testStreamingSelectionOrderByCombineOperatorHonorsDeadline pins the
deadline behaviour specifically: an already-expired deadline must yield an ExceptionResultsBlock
before any child operator is driven.

Known gaps

  • No integration test. This PR touches no file under pinot-integration-tests; the broker-side
    reduce, OFFSET handling, and the streaming response path are covered only at operator level.
    Happy to add one here if reviewers prefer that over a follow-up.
  • The multi-column path rebuilds the second-pass operator chain per emitted equal-prefix run rather
    than per segment, which is a cost cliff on high-cardinality leading sort columns
    (e.g. ORDER BY ts, id). Correct, but worth a follow-up.

Commits

This PR contains 2 commits: the feature, and a separate follow-up commit applying review fixes
(termination/deadline checks in the merge loop, schema-mismatch reporting, segment release on
Error as well as Exception, scan-cost accounting, and the query-option naming above). They are
kept separate so the fixes are reviewable on their own; happy to squash before merge.

Part of #18667.

An unbounded leaf-stage ORDER BY (as injected for sorted merge join inputs)
routes to MinMaxValueBasedSelectionOrderByCombineOperator, which merges every
segment's rows into a single block before returning anything. At large data
volumes this exceeds the leaf stage's CPU budget and ThreadAccountant raises
EarlyTerminationException inside SelectionOperatorUtils.mergeWithOrdering(),
surfacing to the broker as a spurious "Cancelled by sender".

This adds a streaming alternative, opt-in via the `streamingSelectionOrderBy`
query option:

- StreamingSelectionOrderByOperator emits sorted blocks incrementally for a
  segment that is physically sorted on the leading ORDER BY column, reading
  the sorted forward index in order instead of building a priority queue.
- StreamingSelectionOrderByCombineOperator performs a k-way heap merge across
  segment operators and emits bounded blocks (`streamingSelectionOrderByBlockSize`,
  default 10000) rather than one materialized result.
- SelectionPlanNode and CombinePlanNode select these operators when the option
  is set and the sortedness precondition holds; otherwise behaviour is unchanged.

Part of apache#18667.
Correctness and safety fixes on top of the streaming selection ORDER BY
combine operator:

- StreamingSelectionOrderByCombineOperator now checks the query deadline and
  termination state inside the merge loop. The merge drains its heap on the
  caller thread and, for single-block cursors, may never re-enter a child
  operator, so nothing else observed cancellation, pause, or timeout for up to
  (limit + offset) iterations. This restored parity with
  MinMaxValueBasedSelectionOrderByCombineOperator, which this operator replaces
  on the same query shape.
- Child blocks whose data schema disagrees with the merge schema are dropped and
  reported as MERGE_RESPONSE errors instead of being merged blindly, mirroring
  SelectionOrderByResultsBlockMerger. Segments on a server can disagree on
  schema mid-reload, and merging rows of differing width under a single schema
  corrupted the result rather than failing.
- Segments are released when an Error (not just an Exception) escapes the merge,
  so an OutOfMemoryError while accumulating output rows can no longer leave
  segments acquired for the lifetime of the server. In single-stage mode there
  is no start()/stop() backstop around this operator.
- StreamingSelectionOrderByOperator reports docs and entries scanned to the
  shared QueryScanCostContext, as every other selection operator does. Without
  it, scan-based query killing was blind on this path.

Also renames the query option from `streamingSelectionOrderBy` to
`sortedSelectionMergeEnabled` (and `...BlockSize` to
`sortedSelectionMergeBlockSize`). The option gates the classic single-stage
combine path as well as the MSE leaf path, where nothing streams, so the old
name misdescribed it; query option strings are effectively permanent public API.
The default block size moves to a named constant.

Part of apache#18667.
@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.85106% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.57%. Comparing base (a8b207e) to head (5f8d380).
⚠️ Report is 17 commits behind head on master.

Files with missing lines Patch % Lines
...bine/StreamingSelectionOrderByCombineOperator.java 72.41% 39 Missing and 17 partials ⚠️
...rator/query/StreamingSelectionOrderByOperator.java 88.26% 21 Missing and 6 partials ⚠️
...va/org/apache/pinot/core/plan/CombinePlanNode.java 50.00% 0 Missing and 4 partials ⚠️
...pinot/core/plan/maker/InstancePlanMakerImplV2.java 66.66% 1 Missing and 1 partial ⚠️
.../org/apache/pinot/core/plan/SelectionPlanNode.java 92.30% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19120      +/-   ##
============================================
+ Coverage     65.49%   65.57%   +0.08%     
  Complexity     1423     1423              
============================================
  Files          3430     3434       +4     
  Lines        218010   218576     +566     
  Branches      34648    34770     +122     
============================================
+ Hits         142784   143338     +554     
+ Misses        63666    63655      -11     
- Partials      11560    11583      +23     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 ?
java-25 65.57% <80.85%> (+0.08%) ⬆️
temurin 65.57% <80.85%> (+0.08%) ⬆️
unittests 65.57% <80.85%> (+0.08%) ⬆️
unittests1 56.98% <80.85%> (+0.12%) ⬆️
unittests2 37.82% <0.21%> (-0.04%) ⬇️

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants