refactor: make join projection pushdown schema-aware via ColumnIndex/…#23185
refactor: make join projection pushdown schema-aware via ColumnIndex/…#23185Phoenix500526 wants to merge 6 commits into
Conversation
…JoinSide try_pushdown_through_join decided whether each projected join-output column belonged to the left or right child by comparing its output index against the left child's field count, assuming a `left ++ right` output schema. That is wrong for joins whose output is not a plain concatenation -- mark joins append a synthetic `mark` column (JoinSide::None) belonging to neither child -- which is why apache#22902 had to bypass projection pushdown for LeftMark / RightMark. Group the projected columns by the join's output-origin metadata (ColumnIndex.side) instead of output position: Left/Right columns push to the matching child via the child-relative ColumnIndex.index, and a synthetic (JoinSide::None) column makes the pushdown decline so the projection is embedded into the join instead -- no panic, no mis-routing. This lets the LeftMark / RightMark bypass be removed from HashJoinExec and NestedLoopJoinExec. The shared helpers used by cross / symmetric / sort-merge joins (new_join_children, join_table_borders, update_join_on/filter) keep their signatures; the side-grouped path uses a new new_join_children_from_groups and reuses update_join_on/filter with a zero column-index offset. Mark joins still fall back to embedding (pushing their child columns down is a follow-up). Closes apache#23010 Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
09a1d17 to
dca264a
Compare
It is a crate-internal helper, called only by HashJoinExec and NestedLoopJoinExec within datafusion-physical-plan, is not re-exported, and has no external consumers. Reducing its visibility keeps it off the public API surface so future signature changes no longer trip cargo-semver-checks. Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
There was a problem hiding this comment.
@Phoenix500526
Thanks for working on this!
I think the projection pushdown approach makes sense overall, but I have a couple of API surface concerns that I'd like to address before merging.
Remove public column_indices accessors from hash and nested-loop join execution nodes. Projection pushdown now passes the internal column_indices slice from within each impl, avoiding new public API surface and keeping the Vec representation private. Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
There was a problem hiding this comment.
@Phoenix500526
Thanks for the follow-up. The accessor comments look addressed now. I think there is still one blocking semver issue to fix.
Restore the public try_pushdown_through_join signature for semver compatibility. Move the ColumnIndex-aware implementation behind a crate-private helper for join execution internals. Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
kosiew
left a comment
There was a problem hiding this comment.
@Phoenix500526
This iteration looks good to me.
alamb
left a comment
There was a problem hiding this comment.
Thanks @kosiew and @Phoenix500526
| projected_right_child, | ||
| join_filter, | ||
| join_on, | ||
| }) = try_pushdown_through_join( |
There was a problem hiding this comment.
Do we still need try_pushdown_through_join after this PR? Perhaps we should remove it (or migrate other sites over that still use it)?
| })) | ||
| } | ||
|
|
||
| pub(crate) fn try_pushdown_through_join_with_column_indices( |
There was a problem hiding this comment.
this looks pretty similar to try_pushdown_through_join above it -- maybe it could be consolidated so we dn't have two copies 🤔
There was a problem hiding this comment.
try_pushdown_through_join no longer has any in-tree callers. I kept it only because it is public API and removing it was flagged by cargo-semver-checks.
Would you prefer to remove it and treat this as an API change, or keep it as a thin compatibility wrapper that delegates to the ColumnIndex-aware implementation? The wrapper would eliminate the duplicated logic noted below while preserving compatibility.
I can also migrate SymmetricHashJoinExec, since it already stores the required column_indices.
There was a problem hiding this comment.
Would you prefer to remove it and treat this as an API change, or keep it as a thin compatibility wrapper that delegates to the ColumnIndex-aware implementation? The wrapper would eliminate the duplicated logic noted below while preserving compatibility.
I suggest keep as a compatibily wrapper and delegate and then mark it as "deprecate" per https://datafusion.apache.org/contributor-guide/api-health.html#deprecation-guidelines
The ColumnIndex-aware path was added beside the public positional helper to preserve API compatibility, leaving duplicate implementations that could diverge as join output schemas evolve. Keep the public API as a compatibility wrapper over the schema-aware path, and use existing output-origin metadata for symmetric hash joins. This preserves semver compatibility while keeping ownership decisions in one implementation. Refs apache#23010 Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
The public helper infers column ownership from output position, so it cannot represent synthetic join columns. It also encourages callers to rely on the left-then-right schema assumption. Keep it as a compatibility wrapper and expose the ColumnIndex-aware helper as its replacement. Document and validate the public mapping so malformed metadata returns planning errors instead of panicking. Refs apache#23010 Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Which issue does this PR close?
ColumnIndex/JoinSide#23010Rationale for this change
try_pushdown_through_join(datafusion/physical-plan/src/projection.rs)decided whether each projected join-output column belonged to the left or right
child by comparing its output index against the left child's field count
(
join_table_borders) — i.e. it assumed the join output is a plainleft ++ rightconcatenation.That assumption does not hold for all join types. A
LeftMark/RightMarkjoin appends a synthetic
markboolean column that hasJoinSide::Noneandoriginates from neither child, so reasoning from output position can route the
markcolumn to the wrong child. This is the panic that #22902 worked around bydisabling projection pushdown for
LeftMark/RightMarkinHashJoinExecand
NestedLoopJoinExec— correct, but it left the helper reasoning from outputposition rather than from the join's actual output-origin contract.
What changes are included in this PR?
&[ColumnIndex], already computed bybuild_join_schema) intotry_pushdown_through_join.join_table_borders+index < left_field_count) with grouping byColumnIndex.side:Left/Rightcolumns are collected per child using the child-relativeColumnIndex.index;JoinSide::None(synthetic, e.g.mark) column makes the pushdown decline(
Ok(None)), so the projection is embedded into the join instead — no panic,no mis-routing.
LeftMark | RightMarkbypass fromHashJoinExec::try_swapping_with_projectionandNestedLoopJoinExec::try_swapping_with_projection; they now calltry_pushdown_through_joinuniformly.(
new_join_children,join_table_borders,join_allows_pushdown,update_join_on,update_join_filter) keep their signatures. The side-groupedpath uses a new private
new_join_children_from_groupsand reusesupdate_join_on/update_join_filterwith a zero column-index offset (thegroups already carry child-relative indices).
Are these changes tested?
Yes.
subquery.sltcases place a subset/reordering projection over a mark join— hash
LeftMark(INunderOR), negated mark (NOT EXISTSunderOR),and nested-loop mark (non-equi correlated) — asserting both the query result
and the
EXPLAINplan shape.cargo test -p datafusion-physical-plan projection(incl. the filter-pushdown andjoin_table_bordersunit tests),the join
*.sltsuite, andsubquery.sltall pass.cargo clippy -p datafusion-physical-plan -- -D warningsandcargo fmt --allare clean.
Are there any user-facing changes?
No behavior change: the produced physical plans are unchanged for the supported
cases (mark joins keep falling back to the same embedded-projection plan as
before, regular joins push down as before).
One signature change to a
pubhelper:try_pushdown_through_joingains acolumn_indices: &[ColumnIndex]parameter (all in-tree callers are updated). Ifthe project treats this helper as part of the public API surface, please add the
api changelabel.