fix: keep join filter columns when pushing a projection below SortMergeJoinExec - #24593
Conversation
…geJoinExec `SortMergeJoinExec::try_swapping_with_projection` pushed the projection into its children but passed the join filter through unchanged. When the projection did not include a column the filter needs, that column was dropped from the child and the filter's `ColumnIndex` pointed past the end of the batch, panicking in `get_filter_columns`. Use the shared `try_pushdown_through_join_with_column_indices` helper like `HashJoinExec` and `NestedLoopJoinExec` do: it remaps both the join keys and the filter, and declines the pushdown (falling back to embedding the projection in the join) when the filter needs a dropped column. As a side effect, projections containing expressions are now also embedded into the join, which only emits the columns they reference, as already done for the other join operators.
| projected_right_child, | ||
| join_filter, | ||
| join_on, | ||
| }) = try_pushdown_through_join_with_column_indices( |
There was a problem hiding this comment.
try_pushdown_through_join_with_column_indices includes what we have for the old code
| &projection_as_columns[far_left_right_col_ind as _..], | ||
| // Remaps the join keys and the filter's column indices to the | ||
| // projected children, and declines the pushdown if the projection | ||
| // drops a column the filter needs. |
There was a problem hiding this comment.
The overall idea of this PR
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24593 +/- ##
==========================================
+ Coverage 81.39% 81.44% +0.04%
==========================================
Files 1118 1119 +1
Lines 398680 400202 +1522
Branches 398680 400202 +1522
==========================================
+ Hits 324517 325954 +1437
+ Misses 55196 55192 -4
- Partials 18967 19056 +89 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ection-pushdown-filter
kosiew
left a comment
There was a problem hiding this comment.
@jayzhan211, thanks for working on this. The shared schema-aware join helper looks like a good fit here, and the regression coverage addresses the reported SortMergeJoin projection and filter issue. I left one optional suggestion for a little more execution-level coverage. Nothing blocking from my side.
| projected_right_child, | ||
| join_filter, | ||
| join_on, | ||
| }) = try_pushdown_through_join_with_column_indices( |
There was a problem hiding this comment.
Nice improvement here. One optional thought: could we add an execution-level unit test for the successful remapping path, where the filter columns are retained but reordered? The SQL regression covers the fallback with an embedded projection, while the current unit test verifies the rewritten indices without executing the returned SortMergeJoinExec. Executing it would give us a bit more confidence that the remapped JoinFilter schema and columns are consumed correctly. The broader SortMergeJoin spill SQL tests already exercise filtered projected joins, so I see this as a small coverage improvement rather than something that should block this PR.
There was a problem hiding this comment.
Thanks for your review, test added!
Rationale for this change
A sort merge join with a non-equi filter panics (
index out of boundsinget_filter_columns) when the query does not select a column the filter references:The projection pushdown added in #24517 pushes the projection into the join's children but passes
self.filter.clone()through unchanged. The right child no longer producesc, but the filter'sColumnIndexstill points at it.What changes are included in this PR?
SortMergeJoinExec::try_swapping_with_projectionnow uses the sharedtry_pushdown_through_join_with_column_indiceshelper, asHashJoinExecandNestedLoopJoinExecalready do. It remaps both theonkeys and the filter's column indices to the projected children, and declines the pushdown — embedding the projection in the join instead — when the filter needs a column the projection drops.One behavioral side effect: projections containing expressions (e.g.
length(w.p)) are now also embedded into the join, so the join only emits the columns they reference. This matches the existing behavior of the other join operators; the affectedEXPLAIN ANALYZEexpectations insort_merge_join_spill.sltare updated accordingly (projection=[...]on theSortMergeJoinExecline).Are these changes tested?
Yes. A regression test in
joins.sltcovers the panicking case (explain + results). Existing SMJ unit tests, projection tests, proto round-trip tests, andjoins.slt/range_partitioning.slt/sort_merge_join_spill.sltpass.Are there any user-facing changes?
No API changes. Plans for
SortMergeJoinExecunder an expression projection now showprojection=[...]on the join node.