Skip to content

fix: Normalize CollectLeft Joins and similar - #583

Merged
gabotechs merged 18 commits into
datafusion-contrib:mainfrom
paradedb:barbarj.fix-563-just-tests-and-fixes
Aug 8, 2026
Merged

fix: Normalize CollectLeft Joins and similar #583
gabotechs merged 18 commits into
datafusion-contrib:mainfrom
paradedb:barbarj.fix-563-just-tests-and-fixes

Conversation

@barbarj

@barbarj barbarj commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #563

(This is the plan-fix and tests part of #582)

The bug in 563 is actually a failure to enforce a plan shape invariant for multi-task stages: for every stage, executing its plan once per task over the per-task input assignment and unioning the outputs must be equivalent to executing it once over all the data.

This PR applies fixes for the problematic shapes (normalize_collect_joins.rs). Enforcing this invariant will be handled separately. (See discussion in #582).

There are 5 invalid plan shapes to fix. The following strategies are used:

  • HashJoin CollectLeft (join types: Left/LeftSemi/LeftAnti/LeftMark/Full) → change
    CollectLeft->Partitioned (keys exist; no replication needed)
  • HashJoinCollectLeft, null_aware (join type: LeftAnti) -> cap at 1 task
  • NLJ (join types: Left/LeftSemi/LeftAnti/LeftMark) -> call swap_inputs(), then broadcast as usual. (makes the join shape safe for broadcast).
  • NLJ (join type: Full) -> cap at 1 task
  • CrossJoin (always safe with broadcast) -> cap at 1 task if broadcasts are disabled.

The CollectLeft->Partitioned change causes the rewriting of a bunch of plan shapes in the plan tests. The problematic stages run over small data, and thus collapse to a single task during
prepare_network_boundaries, avoiding the bug. Due to the shape normalization pass necessarily running before task counts are decided, we can't see that the task consolidation will happen and must modify the plan.

In-repo benchmarks show equivalent or better performance for all queries, save one: TPCH q22. It drops by about 40% (~17ms -> ~24ms on my machine, 8 workers, 2 threads). As far as I can tell, it's losing a dynamic filter due to being cut into more stages.

@jayshrivastava

Copy link
Copy Markdown
Collaborator

👋🏽 Left comments in the other PR which are relevant in this one #582

@barbarj

barbarj commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

👋🏽 Left comments in the other PR which are relevant in this one #582

Comments addressed. I'm now normalizing Full HashExec joins and ensuring the HashExecJoin's internal properties are maintained with a unit test and the use of HashJoinExecBuilder.

@gabotechs gabotechs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is looking very good!

Just some minor comments, I'd like to have @jayshrivastava chime in as well as he provided very useful comments here.

Comment thread src/distributed_planner/inject_network_boundaries.rs
Comment thread src/distributed_planner/normalize_collect_joins.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs

@gabotechs gabotechs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Flushing a couple more comments as I go

Comment thread src/distributed_planner/inject_network_boundaries.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs
Comment thread tests/multi_task_collect_join_repros.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs Outdated
Comment thread src/distributed_planner/normalize_collect_joins.rs
@barbarj
barbarj force-pushed the barbarj.fix-563-just-tests-and-fixes branch from cb5ca44 to e452e3a Compare August 5, 2026 19:12

@gabotechs gabotechs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @barbarj for this amazing piece of work 🙏! it was definitely a master lesson on how to do joins correctly.

Once we get the CI green, it should be fine to merge.

@barbarj

barbarj commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Once we get the CI green, it should be fine to merge.

Thanks @gabotechs! CI should be green now. (I changed build_side_fetch_is_preserved_by_normalize to assert counts rather than row contents, since distributed execution of that fetch is non-deterministic in regards to which rows survive).

@gabotechs

Copy link
Copy Markdown
Collaborator

Github runners are not in the mood today…

@gabotechs

Copy link
Copy Markdown
Collaborator

Seems like the build_side_fetch_is_preserved_by_normalize test is failing because of some non-determinism in how parquet files are partitioned?

@barbarj, unless you have a better idea, I think we can omit asserting the plan snapshot there.

@barbarj

barbarj commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Seems like the build_side_fetch_is_preserved_by_normalize test is failing because of some non-determinism in how parquet files are partitioned?

@barbarj, unless you have a better idea, I think we can omit asserting the plan snapshot there.

I've got nothing better. Removed!

@gabotechs
gabotechs merged commit 45bd823 into datafusion-contrib:main Aug 8, 2026
31 checks passed
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.

Bug: CollectLeft HashJoin, CrossJoin, and NestedLoopJoin all return incomplete result sets when run over mutliple tasks

3 participants