Add streaming selection ORDER BY combine for physically sorted segments - #19120
Open
rohityadav1993 wants to merge 1 commit into
Open
Add streaming selection ORDER BY combine for physically sorted segments#19120rohityadav1993 wants to merge 1 commit into
rohityadav1993 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19120 +/- ##
============================================
+ Coverage 65.49% 66.61% +1.12%
Complexity 1423 1423
============================================
Files 3430 3443 +13
Lines 218010 218789 +779
Branches 34648 34846 +198
============================================
+ Hits 142784 145747 +2963
+ Misses 63666 61290 -2376
- Partials 11560 11752 +192
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:
|
rohityadav1993
commented
Aug 5, 2026
| _pendingRow = null; | ||
| Object[] row; | ||
| while ((row = nextRow()) != null) { | ||
| if (_primaryComparator.compare(row, runFirstRow) == 0) { |
Contributor
Author
There was a problem hiding this comment.
Self review: This is going to be inefficient when segment sort expression is not same as orderBy experssion and there are too few rows per key.
Consciously keeping it out of scope for now to keep the logic simple for happy path.
rohityadav1993
force-pushed
the
oss/pr1-streaming-selection-combine
branch
2 times, most recently
from
August 5, 2026 19:28
8f83027 to
9c6309a
Compare
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.
rohityadav1993
force-pushed
the
oss/pr1-streaming-selection-combine
branch
from
August 5, 2026 19:55
9c6309a to
3bc0311
Compare
rohityadav1993
marked this pull request as ready for review
August 6, 2026 05:12
Contributor
Author
|
Hi @gortiz, could you help with reviewing this 1st of the 3 PRs. Here is flow chart of the new selection operator being added to help with the reviewflowchart TD
A["getNextBlock()"] --> B{"_exhausted?"}
B -- yes --> Z["return null"]
B -- no --> C{"_tailToSort?"}
C -- "no (no unsorted tail)" --> D["nextSortedRows()"]
C -- "yes (tail must be sorted per-run)" --> E["nextRun()"]
subgraph NSR["nextSortedRows() — pass-through mode"]
D --> D1{"remaining = numRowsToKeep - numRowsEmitted <= 0?"}
D1 -- yes --> D2["return null"]
D1 -- no --> D3["_projectOperator.nextBlock()"]
D3 --> D4{"block == null?"}
D4 -- yes --> D2
D4 -- no --> D5["build BlockValSets + RowBasedBlockValueFetcher\nfor phase1 expressions"]
D5 --> D6["materializeRow() for first min(numDocs, remaining) rows"]
D6 --> D7["numRowsEmitted += rows.size()\nreturn rows"]
end
subgraph NR["nextRun() — buffer-and-sort-per-run mode"]
E --> E1{"remaining <= 0?"}
E1 -- yes --> E2["return null"]
E1 -- no --> E3{"_pendingRow == null?"}
E3 -- yes --> E4["_pendingRow = nextRow()"]
E4 --> E5{"still null?"}
E5 -- yes --> E2
E3 -- no --> E6
E5 -- no --> E6["clear _runHeap;\nseed with _pendingRow as runFirstRow"]
E6 --> E7["loop: row = nextRow()"]
E7 --> E8{"primaryComparator(row, runFirstRow) == 0?"}
E8 -- yes --> E9["add row to _runHeap (bounded to numRowsToKeep)"]
E9 --> E7
E8 -- no --> E10["stash row as _pendingRow (next run's first row)\nbreak loop"]
E10 --> E11["drainAscending(_runHeap)"]
E11 --> E12{"rows.size() > remaining?"}
E12 -- yes --> E13["truncate to first 'remaining' rows"]
E12 -- no --> E14["numRowsEmitted += rows.size()\nreturn rows"]
E13 --> E14
end
subgraph NEXTROW["nextRow() — forward scan cursor"]
F["nextRow()"] --> F1{"current block exhausted?"}
F1 -- yes --> F2{"_projectExhausted?"}
F2 -- yes --> F3["return null"]
F2 -- no --> F4["_projectOperator.nextBlock()"]
F4 --> F5{"block == null?"}
F5 -- yes --> F6["_projectExhausted = true\nreturn null"]
F5 -- no --> F7["rebuild fetcher/docIds/nullBitmaps\nfor new block; reset _currentPos=0"]
F7 --> F1
F1 -- no --> F8["materializeRow() at _currentPos++\nreturn row"]
end
E4 -.calls.-> F
E7 -.calls.-> F
D7 --> G{"_twoPhase?"}
E14 --> G
G -- yes --> H["fetchNonOrderByColumns(rows)"]
G -- no --> I["_dataSchema already built\n(buildSinglePhaseDataSchema in ctor)"]
subgraph PHASE2["fetchNonOrderByColumns() — two-phase second pass"]
H --> H1["collect docIds bitmap from rows"]
H1 --> H2["sort a docId-ordered view sharing same row instances"]
H2 --> H3["BitmapDocIdSetOperator.ascending(docIds)\n→ ProjectionOperator → TransformOperator"]
H3 --> H4["pull transformOperator blocks,\nfill non-order-by values into rows in place"]
H4 --> H5{"_dataSchema == null?"}
H5 -- yes --> H6["buildTwoPhaseDataSchema()"]
H5 -- no --> H7["done"]
H6 --> H7
end
H7 --> J
I --> J["new SelectionResultsBlock(_dataSchema, rows, _comparator, _queryContext)"]
J --> K["return block"]
|
gortiz
requested review from
Jackie-Jiang,
gortiz and
yashmayya
and removed request for
gortiz and
yashmayya
August 6, 2026 09:08
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.
featureperformancerelease-notesSummary
Instead of computing a segment's entire top-K at once like
SelectionOrderByOperator, return results incrementally, oneSelectionResultsBlockpergetNextBlock()call, so a downstream merge/combine stage can lazily pull rows from multiple segments, stop early once it has enough global results, and optionally k-way merge-sort blocks before sending downstream.Problem
An unbounded leaf-stage
ORDER BY(the shape injected below a sorted merge join input, and also reachable directly) routes toMinMaxValueBasedSelectionOrderByCombineOperator, which merges every segment's rows into a single materialized block before returning anything. At large data volumes this can exceed the leaf stage's CPU budget andThreadAccountantraisesEarlyTerminationExceptioninsideSelectionOperatorUtils.mergeWithOrdering(), surfacing at the broker as a spuriousCancelled 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 BYcolumn.Approach
StreamingSelectionOrderByOperator(new): emits sorted blocks incrementally for a segment physically sorted on the leadingORDER BYcolumn, walking the sorted forward index in order instead of filling a priority queue with the whole segment. Multi-columnORDER BYis handled by a second pass over each equal-prefix run.StreamingSelectionOrderByCombineOperator(new): k-way heap merge across the per-segment operators, emitting bounded blocks instead of one materialized result. Segments are acquired/released incrementally; the merge loop does periodic termination/deadline/resource-usage sampling, matchingBaseStreamingCombineOperator.SelectionPlanNode,CombinePlanNode,InstancePlanMakerImplV2,QueryContext: select these new operators only when the option is set and the sortedness precondition holds; otherwise fall back to the existing operators.SelectionOrderByResultsBlockMerger.Opt-in query options
sortedSelectionMergeEnabledfalsesortedSelectionMergeBlockSize10000Broker.DEFAULT_SORTED_SELECTION_MERGE_BLOCK_SIZE)No behavior 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
Unit tests
64 tests, all green:
StreamingSelectionOrderByOperatorTest(new)StreamingSelectionOrderByCombineOperatorTest(new)CombineSlowOperatorsTest(extended)QueryOptionsUtilsTest(extended)CombineSlowOperatorsTest.testStreamingSelectionOrderByCombineOperatorHonorsDeadlinepins deadline behavior: an already-expired deadline must yield anExceptionResultsBlockbefore any child operator is driven.Local cluster test
This PR's leaf-stage combine has no external effect on its own that an explain plan can surface — it's observable in the block sizes the sorted-mailbox-merge receiver (PR2: #19121) reports consuming. Verified by exercising the full stack with
streamingSortedMailboxReceive=true(later PR) on a colocated sorted join (/*+ joinOptions(join_strategy='sorted', is_colocated_by_join_keys='true') */, withjoin_strategy='sorted'both set and removed), against 3,144,172 docs across 2 segments in a local table on 4 servers / 2 replicas.Validation: streaming sorted selection
Sorted merge join query:
Explain plan: a top-level
LogicalSort(fetch=10)over aPinotLogicalSortExchange, feeding aLogicalJoinwhose two inputs are eachPinotLogicalSortExchange(isSortOnSender=true)wrapping aLogicalSortover a filtered scan ofmytable— i.e. both join inputs are sorted before the sort-exchange.Full stageStats: https://gist.github.com/rohityadav1993/2c0b8f5bb37dc4de9df1cde967a35dc8
Result
stageStats(query returns 10 rows): topMAILBOX_RECEIVE(stage-1 output) →MAILBOX_SEND/SORT_OR_LIMIT→ a secondMAILBOX_RECEIVE(fanIn 2) →MAILBOX_SEND/SORT_OR_LIMIT/TRANSFORM→SORTED_MERGE_JOIN(emittedRows 3629), whose two inputs areMAILBOX_RECEIVEat stage 3 and stage 4, each withkWayMergeUsed: trueandemittedRows: 20000. Each of those receives from anEMPTY_MAILBOX_SENDwith no stats (flagged in the PR as a bug to raise separately in PR3: early termination isn't propagated, resulting in empty stats for that child).Note: stage 3 receiving 20000 events from each side implies the 1000 default
DEFAULT_SORTED_SELECTION_MERGE_BLOCK_SIZEand 2 workers (2 segments); total across both sides is 40000emittedRows.Corresponding non-streaming sorted-selection query (same query shape but
sortedSelectionMergeEnabled=false,streamingSortedMailboxReceive=false,enableTrace=true,join_strategyhint removed, and requiringSET maxRowsInJoin = 10485770;to bypass hashjoin guardrails): the accompanyingMAILBOX_RECEIVEstageStats showsemittedRows: 3144172— significantly higher than the streaming case, since the whole dataset is materialized rather than streamed in bounded blocks.Conclusion: achieving an efficient sorted merge join requires a corresponding streaming sorted-selection operator at the leaf stage.
stageStatsshowed the two join-inputMAILBOX_RECEIVEoperators (one per server holding a matching partition) each reportingkWayMergeUsed: trueand closing exactly oneemittedRows: 10000block (streamingSortedMailboxReceiveBlockSize's default), for 20,000 rows fetched in bounded blocks across the 2 servers.Remote cluster test
Skipped; thorough benchmarks with sorted merge join and cluster tests will be captured in later PRs.
Known gaps
Part of #18667.
Follow up PRs wip:
#19121
#19122