Repository navigation
JoinSelection should check if inputs already satisfy co-partitioning requirements before choosing CollectLeft (broadcast) #25301
Description
Is your feature request related to a problem or challenge?
When executing a hash join where both input tables are already physically co-partitioned on the join keys (for example, via matching Partitioning::Range split points or compatible Partitioning::Hash), the optimal execution mode is PartitionMode::Partitioned because it achieves a 0-shuffle, task-local join.
However, in JoinSelection::try_collect_left, JoinSelection only consults size thresholds (supports_collect_by_thresholds against hash_join_single_partition_threshold / hash_join_single_partition_threshold_rows). If the build side is smaller than the threshold (default: 131,072 rows / 1 MB), JoinSelection unconditionally selects PartitionMode::CollectLeft (broadcast):
datafusion/datafusion/physical-optimizer/src/join_selection.rs
Lines 301 to 312 in f40d978
| let transformed = if let Some(hash_join) = plan.downcast_ref::<HashJoinExec>() { | |
| match hash_join.partition_mode() { | |
| PartitionMode::Auto => try_collect_left(hash_join, false, context)? | |
| .map_or_else( | |
| || partitioned_hash_join(hash_join, context).map(Some), | |
| |v| Ok(Some(v)), | |
| )?, | |
| PartitionMode::CollectLeft => try_collect_left(hash_join, true, context)? | |
| .map_or_else( | |
| || partitioned_hash_join(hash_join, context).map(Some), | |
| |v| Ok(Some(v)), | |
| )?, |
In distributed engines (such as DataFusion-Distributed or downstream engines like ParadeDB), CollectLeft inserts a CoalescePartitions / network broadcast across all worker tasks, serializing and transmitting the table across the network. If both sides were already co-partitioned with identical split points / partition boundaries, broadcasting is strictly worse than a local partition-aligned join:
- A co-partitioned
Partitionedjoin requires 0 network shuffles and 0 broadcast overhead. - Degrading to
CollectLeftintroduces unnecessary network broadcast, data duplication, and memory amplification.
Describe the solution you'd like
Before selecting PartitionMode::CollectLeft in JoinSelection (or within try_collect_left), check whether the join inputs already satisfy the join's distribution requirements for PartitionMode::Partitioned on the join keys.
If the inputs already satisfy the partitioned join requirement (i.e. they are already co-partitioned):
- Preserve
PartitionMode::Partitionedrather than degrading toCollectLeft. - Only fall back to
CollectLeftwhen the inputs are not co-partitioned (where broadcasting the smaller side avoids a 2-sided repartition shuffle).
Describe alternatives you've considered
Currently, downstream systems must either:
- Globally disable broadcast joins by setting
hash_join_single_partition_threshold_rows = 0(which harms queries joining against small, unpartitioned dimension tables where broadcast is genuinely optimal), or - Implement a custom downstream physical optimizer rule that inspects
CollectLeftjoins, verifies whether both children share compatiblePartitioningon the join keys, and mutates the join mode back toPartitionMode::PartitionedbeforeEnsureRequirementsruns.
Handling this directly within JoinSelection would benefit all users and downstream engines querying partitioned or co-partitioned tables.
Additional context
- Relevant lines in
JoinSelection: - Related PRs extending range partitioning and distribution satisfaction:
- PR Adapt existing partitioning for unsatisfied inputs in co-partitioned joins #24600 / PR feat: allow scaling RangePartitioning #24766 (Preserve and adapt range partitioning in
EnforceDistribution).
- PR Adapt existing partitioning for unsatisfied inputs in co-partitioned joins #24600 / PR feat: allow scaling RangePartitioning #24766 (Preserve and adapt range partitioning in
Activity
stuhood commentedon Sep 14, 2026
/take
gene-bordegaray commentedon Sep 16, 2026
thanks @stuhood
what would you think about running some / sharing some benches in DFD? I see the point here, what I am thinking is that a partitioned hash join is going to have more build tables, would it ever be the case that we keep the collect left but do not broadcast thus we don't have network overhead and don't risk having things like build / read amp?
stuhood commentedon Sep 17, 2026
datafusion-contrib/datafusion-distributed#730 is almost ready for review to add these on the
df-dside. In ParadeDB, I added an optimizer rule to convertCollectLeftback toPartitionedfor co-partitioned joins (basically, working around this Isssue): seeScenario 1in https://github.com/paradedb/paradedb/pull/6342/changes#diff-89dfce63e3efead59030ea110c105379e915189aaa282cac639adb2981da39f8The co-partitioned join ends up with a smaller hash table per partition: it also frequently is able to apply the perfect hash join optimization dynamically at runtime, because the join keys have narrow
min/maxbounds.Not sure what you mean here.
perf: Optimize range partitioning across multi-key and asymmetric joi…