You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ballista shuffles data on every join whose keys don't already match the input partitioning, because the planner has no way to learn that a table is hash-bucketed by some column. For workloads on pre-bucketed data (e.g. Iceberg / Parquet datasets where producers already hash-partition by the join key), the network shuffle is pure overhead: the data is already co-located, the planner just doesn't know it.
Concretely:
BallistaQueryPlanner ships the logical plan to the scheduler.
AdaptivePlanner runs DataFusion's physical planner + a custom rule chain ending in DistributedExchangeRule, which inserts an ExchangeExec above every RepartitionExec(Hash).
DefaultDistributedPlanner cuts a stage at every shuffle.
The only join-aware rule today is JoinSelection, which only swaps build sides based on row/byte stats — it has no notion of partition co-location.
The catalog has no concept of hash bucketing; only ListingTable directory partitions exist.
Proposed solution
Bring three Pinot V2 Physical Optimizer ideas to Ballista (Pinot's RelToPRelConverter → WorkerExchangeAssignmentRule):
Colocated joins — when both join inputs declare matching hash bucketing on the join keys, skip the shuffle entirely and let the join's required hash distribution be satisfied directly by the inputs.
Sub-partitioning for divisor bucket counts (e.g. 16 vs 8) — locally coalesce the larger side instead of shuffling, since (hash(k) % 16) % 8 == hash(k) % 8.
Small-side broadcast in the AQE path — when one side fits under a configured byte threshold, promote a PartitionedHashJoinExec to CollectLeft. (This complements PR feat(scheduler): broadcast-style hash join for small-side joins #1647, which added the same lowering to the non-AQE DefaultDistributedPlanner; the AQE planner has no equivalent today, per the TODO at ballista/scheduler/src/state/aqe/mod.rs.)
Architecture
A small metadata trait in ballista-core lets any TableProvider declare on-disk hash bucketing without a DataFusion fork. Two new PhysicalOptimizerRules slot into the existing AdaptivePlanner rule chain just before DistributedExchangeRule:
A full PRel-style multi-pass optimizer rewrite (reuse DataFusion's Partitioning + EquivalenceProperties instead of inventing a BallistaDataDistribution).
Custom DDL grammar — piggyback on DataFusion's existing OPTIONS (...) clause and a PartitionedTableProvider wrapper.
Iceberg / Delta integration (design the trait so they can plug in later).
Problem
Ballista shuffles data on every join whose keys don't already match the input partitioning, because the planner has no way to learn that a table is hash-bucketed by some column. For workloads on pre-bucketed data (e.g. Iceberg / Parquet datasets where producers already hash-partition by the join key), the network shuffle is pure overhead: the data is already co-located, the planner just doesn't know it.
Concretely:
BallistaQueryPlannerships the logical plan to the scheduler.AdaptivePlannerruns DataFusion's physical planner + a custom rule chain ending inDistributedExchangeRule, which inserts anExchangeExecabove everyRepartitionExec(Hash).DefaultDistributedPlannercuts a stage at every shuffle.JoinSelection, which only swaps build sides based on row/byte stats — it has no notion of partition co-location.ListingTabledirectory partitions exist.Proposed solution
Bring three Pinot V2 Physical Optimizer ideas to Ballista (Pinot's
RelToPRelConverter→WorkerExchangeAssignmentRule):(hash(k) % 16) % 8 == hash(k) % 8.PartitionedHashJoinExectoCollectLeft. (This complements PR feat(scheduler): broadcast-style hash join for small-side joins #1647, which added the same lowering to the non-AQEDefaultDistributedPlanner; the AQE planner has no equivalent today, per the TODO atballista/scheduler/src/state/aqe/mod.rs.)Architecture
A small metadata trait in
ballista-corelets anyTableProviderdeclare on-disk hash bucketing without a DataFusion fork. Two newPhysicalOptimizerRules slot into the existingAdaptivePlannerrule chain just beforeDistributedExchangeRule:Goals
Non-goals
Partitioning+EquivalencePropertiesinstead of inventing aBallistaDataDistribution).OPTIONS (...)clause and aPartitionedTableProviderwrapper.JoinSelection's cost model.Implementation
PR #1676.
Follow-up
#1679 — extend
ColocatedJoinRuleandBroadcastSmallSideRuletoSortMergeJoinExec, since PR #1651 made sort-merge the default join.