Optimize logical optimizer: skip map_subqueries + in-place rewriting#22298
Open
adriangb wants to merge 1 commit into
Open
Optimize logical optimizer: skip map_subqueries + in-place rewriting#22298adriangb wants to merge 1 commit into
adriangb wants to merge 1 commit into
Conversation
Three optimizations that together yield ~23-25% faster optimization on TPC-H/TPC-DS and up to 33% on expression-heavy queries: 1. map_subqueries short-circuit: skip expression tree walks when no subquery expressions exist. Previously rewrite_with_subqueries called map_subqueries at every plan node, walking all expression trees via ownership-based transform_down even with no subqueries. 2. plan_has_subqueries per-pass check: when no subqueries exist in the plan, bypass rewrite_with_subqueries entirely and use the cheaper rewrite_plan_in_place path. 3. rewrite_plan_in_place with Arc::make_mut: new map_children_mut method that mutates children in-place, avoiding the Arc::unwrap_or_clone + Arc::new allocation cycle at every node. The owned-plan rule API is bridged with std::mem::take, which is allocation-free: LogicalPlan::default() is an EmptyRelation that shares the process-wide static empty schema. Also adds optimizer-only benchmarks that isolate optimizer performance from SQL parsing and analysis overhead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Supersedes #20837. That PR's branch was rebased onto current
main; GitHub doesnot allow reopening a closed PR once its branch has been force-pushed, so this is
a fresh PR carrying the same work, rebased and re-benchmarked.
Rationale for this change
Optimizer::optimizeruns every logical optimizer rule over the whole plan,repeatedly, until a fixed point. Benchmarking the optimizer in isolation (SQL
parsing and analysis excluded) surfaced two avoidable costs:
rewrite_with_subqueriescallsmap_subqueriesat every plan node, walkingevery expression tree via ownership-based
transform_down— even for planswith no subquery expressions at all, which is the common case.
TreeNode::rewritetraversal performs anArc::unwrap_or_clone+Arc::newcycle at every child node, re-allocatingthe
Arcspine on every pass even when nothing changes.What changes are included in this PR?
Three optimizations:
map_subqueriesshort-circuit — skip the expression-tree walk when a nodehas no subquery expressions.
plan_has_subqueriesper-pass check — when the whole plan has nosubqueries, bypass
rewrite_with_subqueriesentirely and use the cheaperin-place traversal.
rewrite_plan_in_placewithArc::make_mut— a newLogicalPlan::map_children_mutmutatesArc<LogicalPlan>children in place(copy-on-write; free when the refcount is 1, which it always is in the
optimizer), avoiding the
Arc::unwrap_or_clone+Arc::newcycle. Theowned-plan rule API is bridged with
std::mem::take, which isallocation-free because
LogicalPlan::default()is anEmptyRelationthatshares the process-wide static empty schema.
Also adds optimizer-only benchmarks to
sql_planner.rsthat isolate optimizercost from SQL parsing/analysis.
Benchmark results (optimizer-only, criterion, this PR vs
main)Measured A/B on a single machine: for the baseline run the two optimizer rule
files were reverted to
main(keeping the new benchmark code), then restored, soonly the optimization itself is being measured. The
optimizer_tpcds_allnumberwas confirmed across multiple back-to-back runs after an initial reading was
distorted by machine interference.
Possible future work (not in this PR)
The
mem::takebridge inrewrite_plan_in_placeis allocation-free, but it stillextracts an owned plan for every
(node, rule)pair up front — before the ruledecides whether it will transform — and the overwhelming majority of those pairs
are no-ops. A dynamic "lazy handle" rule API (the rule receives a handle that
derefs to
&LogicalPlanfor free and only pays the copy-on-write / move when itcalls
make_mut/replace) would let the framework skip the extraction forno-op rule invocations entirely. That is a breaking change to the public
OptimizerRuletrait and is out of scope here; it is being prototyped separately.Are these changes tested?
Yes. The existing optimizer test suite (713 tests across
datafusion-optimizer)passes unchanged. The optimizations are behavior-preserving: the in-place
traversal produces the same plans as the ownership-based traversal, and the
subquery short-circuit only skips work that is provably a no-op.
Are there any user-facing changes?
No breaking API changes.
LogicalPlan::map_children_mutis a new public method —purely additive. Optimization is faster; output plans are unchanged.