Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a329cf3
feat: cost-based join order enumeration in JoinSelection
Dandandan Aug 18, 2026
aa652b5
fix: require a margin before replacing the planner's join order
Dandandan Aug 18, 2026
de80dd7
fix: estimate a filter one conjunct at a time, and estimate IN lists
Dandandan Aug 18, 2026
1040be5
refactor: trim join enumeration comments, drop the greedy fallback
Dandandan Aug 18, 2026
1a3ab0c
fix: keep the enumeration module private and drop unused async from t…
Dandandan Aug 18, 2026
47f3a39
fix: don't re-enumerate inside a subtree whose order was kept
Dandandan Aug 18, 2026
074cd43
refactor: make join enumeration its own rule
Dandandan Aug 18, 2026
53da44d
docs: add join_enumeration to the optimizer rule reference
Dandandan Aug 19, 2026
f37ebb5
feat: enumerate sort merge join orders too
Dandandan Aug 19, 2026
636b455
feat: enumerate cross and nested loop join orders too
Dandandan Aug 19, 2026
4c34486
Cost partitioning in join enumeration, raise the collect threshold
Dandandan Aug 19, 2026
cf466ca
Fix two costs the search got wrong for sort merge and keyless joins
Dandandan Aug 19, 2026
bae4492
Merge upstream/main into perf/join-order-enumeration
Dandandan Aug 19, 2026
f4a3d75
Merge remote-tracking branch 'upstream/main' into perf/join-order-enu…
Dandandan Aug 20, 2026
3496d02
Trim the comments this PR added
Dandandan Aug 20, 2026
29c224d
Narrow each rebuilt sort merge join instead of once above the subtree
Dandandan Aug 20, 2026
8126ad3
Make the cost model a trait others can plug into
Dandandan Aug 21, 2026
a61d01b
Move the join graph into its own module
Dandandan Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion datafusion/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,22 @@ config_namespace! {
/// query is used.
pub join_reordering: bool, default = true

/// When set to true, the physical plan optimizer enumerates join orders for
/// subtrees of joins and picks the cheapest from cardinality estimates,
/// considering bushy shapes as well as left-deep ones. Subtrees whose inputs
/// lack row count statistics are left untouched.
pub join_enumeration: bool, default = true

/// How much cheaper an enumerated join order must be, in percent, before it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this set of settings is exactly why I am hesitant to put something too complicated into the datafusion core -- the join ordering algorithms can get so out of hand complicated I think we need to find a way to keep the core (relatively) simple and leave API hooks to support the more complicated cases

I have more suggestions on how to do this below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah - my goal would be to mostly bring this down as well (i.e. remove or accept the regression).

/// replaces the order the planner produced. Estimates are often too close to tell
/// two orders apart, so a small gain is not worth acting on.
pub join_enumeration_min_improvement: u8, default = 10

/// Maximum inputs in a join subtree for which `join_enumeration` searches, at a
/// cost of `O(3^n)`. Larger subtrees keep the planner's order, as do subtrees of
/// more than 16 inputs regardless of this setting.
pub join_enumeration_limit: usize, default = 12

/// When set to true, the physical plan optimizer uses the pluggable
/// `StatisticsRegistry` for statistics propagation across operators.
/// This enables more accurate cardinality estimates compared to each
Expand All @@ -1689,7 +1705,7 @@ config_namespace! {

/// The maximum estimated size in bytes for one input side of a HashJoin
/// will be collected into a single partition
pub hash_join_single_partition_threshold: usize, default = 1024 * 1024
pub hash_join_single_partition_threshold: usize, default = 4 * 1024 * 1024

/// The maximum estimated size in rows for one input side of a HashJoin
/// will be collected into a single partition
Expand Down
39 changes: 20 additions & 19 deletions datafusion/core/src/optimizer_rule_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,23 @@ in multiple phases.
| ----- | ------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| 1 | `OutputRequirements` | add phase | Adds helper nodes so output requirements survive later physical rewrites. |
| 2 | `aggregate_statistics` | - | Uses exact source statistics to answer some aggregates without scanning data. |
| 3 | `join_selection` | - | Chooses join implementation, build side, and partition mode from statistics and stream properties. |
| 4 | `LimitedDistinctAggregation` | - | Pushes limit hints into grouped distinct-style aggregations when only a small result is needed. |
| 5 | `FilterPushdown` | pre-optimization phase | Pushes supported physical filters down toward data sources before distribution and sorting are enforced. |
| 6 | `WindowTopN` | - | Replaces eligible row-number window and filter patterns with per-partition TopK execution. |
| 7 | `EnsureRequirements` | - | Enforces both distribution and sorting requirements in a single idempotent rule. |
| 8 | `CombinePartialFinalAggregate` | - | Collapses adjacent partial and final aggregates when the distributed shape makes them redundant. |
| 9 | `OptimizeAggregateOrder` | - | Updates aggregate expressions to use the best ordering once sort requirements are known. |
| 10 | `ProjectionPushdown` | early pass | Pushes projections toward inputs before later physical rewrites add more limit and TopK structure. |
| 11 | `OutputRequirements` | remove phase | Removes the temporary output-requirement helper nodes after requirement-sensitive planning is done. |
| 12 | `LimitAggregation` | - | Passes a limit hint into eligible aggregations so they can keep fewer accumulator buckets. |
| 13 | `LimitPushPastWindows` | - | Pushes fetch limits through bounded window operators when doing so keeps the result correct. |
| 14 | `HashJoinBuffering` | - | Adds buffering on the probe side of hash joins so probing can start before build completion. |
| 15 | `LimitPushdown` | - | Moves physical limits into child operators or fetch-enabled variants to cut data early. |
| 16 | `TopKRepartition` | - | Pushes TopK below hash repartition when the partition key is a prefix of the sort key. |
| 17 | `ProjectionPushdown` | late pass | Runs projection pushdown again after limit and TopK rewrites expose new pruning opportunities. |
| 18 | `PushdownSort` | - | Pushes sort requirements into data sources that can already return sorted output. |
| 19 | `EnsureCooperative` | - | Wraps non-cooperative plan parts so long-running tasks yield fairly. |
| 20 | `FilterPushdown(Post)` | post-optimization phase | Pushes dynamic filters at the end of optimization, after plan references stop moving. |
| 21 | `SanityCheckPlan` | - | Validates that the final physical plan meets ordering, distribution, and infinite-input safety requirements. |
| 3 | `join_enumeration` | - | Chooses the join tree shape by costing alternative orders from statistics. |
| 4 | `join_selection` | - | Chooses join implementation, build side, and partition mode from statistics and stream properties. |
| 5 | `LimitedDistinctAggregation` | - | Pushes limit hints into grouped distinct-style aggregations when only a small result is needed. |
| 6 | `FilterPushdown` | pre-optimization phase | Pushes supported physical filters down toward data sources before distribution and sorting are enforced. |
| 7 | `WindowTopN` | - | Replaces eligible row-number window and filter patterns with per-partition TopK execution. |
| 8 | `EnsureRequirements` | - | Enforces both distribution and sorting requirements in a single idempotent rule. |
| 9 | `CombinePartialFinalAggregate` | - | Collapses adjacent partial and final aggregates when the distributed shape makes them redundant. |
| 10 | `OptimizeAggregateOrder` | - | Updates aggregate expressions to use the best ordering once sort requirements are known. |
| 11 | `ProjectionPushdown` | early pass | Pushes projections toward inputs before later physical rewrites add more limit and TopK structure. |
| 12 | `OutputRequirements` | remove phase | Removes the temporary output-requirement helper nodes after requirement-sensitive planning is done. |
| 13 | `LimitAggregation` | - | Passes a limit hint into eligible aggregations so they can keep fewer accumulator buckets. |
| 14 | `LimitPushPastWindows` | - | Pushes fetch limits through bounded window operators when doing so keeps the result correct. |
| 15 | `HashJoinBuffering` | - | Adds buffering on the probe side of hash joins so probing can start before build completion. |
| 16 | `LimitPushdown` | - | Moves physical limits into child operators or fetch-enabled variants to cut data early. |
| 17 | `TopKRepartition` | - | Pushes TopK below hash repartition when the partition key is a prefix of the sort key. |
| 18 | `ProjectionPushdown` | late pass | Runs projection pushdown again after limit and TopK rewrites expose new pruning opportunities. |
| 19 | `PushdownSort` | - | Pushes sort requirements into data sources that can already return sorted output. |
| 20 | `EnsureCooperative` | - | Wraps non-cooperative plan parts so long-running tasks yield fairly. |
| 21 | `FilterPushdown(Post)` | post-optimization phase | Pushes dynamic filters at the end of optimization, after plan references stop moving. |
| 22 | `SanityCheckPlan` | - | Validates that the final physical plan meets ordering, distribution, and infinite-input safety requirements. |
Loading