Repository navigation
Description
Describe the bug
This is a followup to #13748 which was largely addressed by #16217 however I've noticed in my testing that with large enough # of columns planning time is still growing exponentially. Below is the output from a modified version of the sql_planner using a branch from DF 49:
physical_sorted_union_order_by_10
time: [12.089 ms 12.105 ms 12.125 ms]
change: [+6.4475% +6.6567% +6.8895%] (p = 0.00 < 0.05)
Performance has regressed.
physical_sorted_union_order_by_50
time: [323.92 ms 324.75 ms 325.53 ms]
change: [+6.5657% +6.8547% +7.1319%] (p = 0.00 < 0.05)
Performance has regressed.
Benchmarking physical_sorted_union_order_by_100: Warming up for 30.000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 18.5s.
physical_sorted_union_order_by_100
time: [1.7079 s 1.7143 s 1.7221 s]
change: [+6.5417% +6.9412% +7.5811%] (p = 0.00 < 0.05)
Performance has regressed.
Found 1 outliers among 10 measurements (10.00%)
1 (10.00%) high mild
Benchmarking physical_sorted_union_order_by_200: Warming up for 30.000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 118.6s.
physical_sorted_union_order_by_200
time: [10.271 s 10.321 s 10.400 s]
change: [+4.3930% +4.9196% +5.7641%] (p = 0.00 < 0.05)
Performance has regressed.
Found 3 outliers among 10 measurements (30.00%)
2 (20.00%) low mild
1 (10.00%) high severe
Benchmarking physical_sorted_union_order_by_500: Warming up for 30.000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 1350.0s.
physical_sorted_union_order_by_500
time: [129.75 s 131.00 s 132.82 s]
Ignored the regressed messages - tests are +-10% on my machine between runs. Note the time for the 500 columns.
Below is the output from one of the benchmark runs with 500 columns where I instrumented the logical plan optimize and physical plan optimizer to output timings:
Benchmarking physical_sorted_union_order_by_500: Collecting 10 samples in estimated 1202.6 s (10 iterations)[2025-08-20T14:03:18Z INFO datafusion_optimizer::analyzer] Analyzer::execute_and_check rule resolve_grouping_function took 46 ms
[2025-08-20T14:03:20Z INFO datafusion_optimizer::analyzer] Analyzer::execute_and_check rule type_coercion took 2007 ms
[2025-08-20T14:03:20Z INFO datafusion_optimizer::analyzer] Analyzer::execute_and_check rule processing took 2054 ms
[2025-08-20T14:03:20Z INFO datafusion_optimizer::analyzer] Analyzer::execute_and_check took 2129 ms
[2025-08-20T14:03:21Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_nested_union took > 50ms: 1544 ms
[2025-08-20T14:03:22Z INFO datafusion_optimizer::optimizer] Optimization for rule simplify_expressions took > 50ms: 823 ms
[2025-08-20T14:03:22Z INFO datafusion_optimizer::optimizer] Optimization for rule replace_distinct_aggregate took > 50ms: 66 ms
[2025-08-20T14:03:22Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_join took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule decorrelate_predicate_subquery took > 50ms: 130 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule scalar_subquery_to_join took > 50ms: 86 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule decorrelate_lateral_join took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule extract_equijoin_predicate took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_duplicated_expr took > 50ms: 68 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_filter took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_limit took > 50ms: 66 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule propagate_empty_relation took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_one_union took > 50ms: 68 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule filter_null_join_keys took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_outer_join took > 50ms: 68 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule push_down_limit took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule push_down_filter took > 50ms: 67 ms
[2025-08-20T14:03:23Z INFO datafusion_optimizer::optimizer] Optimization for rule single_distinct_aggregation_to_group_by took > 50ms: 67 ms
[2025-08-20T14:03:24Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_group_by_constant took > 50ms: 68 ms
[2025-08-20T14:03:24Z INFO datafusion_optimizer::optimizer] Optimization for rule common_sub_expression_eliminate took > 50ms: 138 ms
[2025-08-20T14:03:25Z INFO datafusion_optimizer::optimizer] Optimization for rule optimize_projections took > 50ms: 1721 ms
[2025-08-20T14:03:26Z INFO datafusion_optimizer::optimizer] Processing time for pass 0 was 5561ms
[2025-08-20T14:03:27Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_nested_union took > 50ms: 1648 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule simplify_expressions took > 50ms: 804 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule replace_distinct_aggregate took > 50ms: 72 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_join took > 50ms: 75 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule decorrelate_predicate_subquery took > 50ms: 135 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule scalar_subquery_to_join took > 50ms: 91 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule decorrelate_lateral_join took > 50ms: 76 ms
[2025-08-20T14:03:28Z INFO datafusion_optimizer::optimizer] Optimization for rule extract_equijoin_predicate took > 50ms: 72 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_duplicated_expr took > 50ms: 75 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_filter took > 50ms: 72 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_limit took > 50ms: 74 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule propagate_empty_relation took > 50ms: 72 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_one_union took > 50ms: 74 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule filter_null_join_keys took > 50ms: 72 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_outer_join took > 50ms: 74 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule push_down_limit took > 50ms: 71 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule push_down_filter took > 50ms: 75 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule single_distinct_aggregation_to_group_by took > 50ms: 73 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule eliminate_group_by_constant took > 50ms: 75 ms
[2025-08-20T14:03:29Z INFO datafusion_optimizer::optimizer] Optimization for rule common_sub_expression_eliminate took > 50ms: 175 ms
[2025-08-20T14:03:31Z INFO datafusion_optimizer::optimizer] Optimization for rule optimize_projections took > 50ms: 1745 ms
[2025-08-20T14:03:31Z INFO datafusion_optimizer::optimizer] optimizer pass 1 did not make changes. Processing time: 5826 ms
[2025-08-20T14:03:31Z INFO datafusion_optimizer::optimizer] Processing time for 3 passes was 11473ms
[2025-08-20T14:03:31Z INFO datafusion_optimizer::optimizer] Optimizer took 11559 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Creating initial physical plan took 15814 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Physical plan optimizer OutputRequirements took 0 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Physical plan optimizer aggregate_statistics took 0 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Physical plan optimizer join_selection took 0 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Physical plan optimizer LimitedDistinctAggregation took 0 ms
[2025-08-20T14:03:47Z INFO datafusion::physical_planner] Physical plan optimizer FilterPushdown took 18 ms
[2025-08-20T14:04:52Z INFO datafusion::physical_planner] Physical plan optimizer EnforceDistribution took 64265 ms
[2025-08-20T14:04:52Z INFO datafusion::physical_planner] Physical plan optimizer CombinePartialFinalAggregate took 0 ms
[2025-08-20T14:05:02Z INFO datafusion::physical_planner] Physical plan optimizer EnforceSorting took 10527 ms
[2025-08-20T14:05:02Z INFO datafusion::physical_planner] Physical plan optimizer OptimizeAggregateOrder took 0 ms
[2025-08-20T14:05:04Z INFO datafusion::physical_planner] Physical plan optimizer ProjectionPushdown took 2373 ms
[2025-08-20T14:05:04Z INFO datafusion::physical_planner] Physical plan optimizer coalesce_batches took 3 ms
[2025-08-20T14:05:04Z INFO datafusion::physical_planner] Physical plan optimizer coalesce_async_exec_input took 0 ms
[2025-08-20T14:05:04Z INFO datafusion::physical_planner] Physical plan optimizer OutputRequirements took 0 ms
[2025-08-20T14:05:04Z INFO datafusion::physical_planner] Physical plan optimizer LimitAggregation took 0 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Physical plan optimizer LimitPushdown took 2122 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Physical plan optimizer ProjectionPushdown took 5 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Physical plan optimizer EnsureCooperative took 0 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Physical plan optimizer FilterPushdown(Post) took 79 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Physical plan optimizer SanityCheckPlan took 63 ms
[2025-08-20T14:05:07Z INFO datafusion::physical_planner] Optimize physical plan took 79736 ms
Note the times for the logical plan optimizer eliminate_nested_union and the physical plan initial plan create_initial_plan and optimizers EnforceDistribution, EnforceSorting, ProjectionPushdown and LimitPushdown.
To Reproduce
No response
Expected behavior
Query planning should be much faster.
Additional context
No response
Activity
alamb commentedon Aug 20, 2025
Thank you @Omega359 -- I think other people are interested in this topic too and we should make things better in this respect
Here are some related tickets:
My suggested next steps are:
alamb commentedon Sep 9, 2025
We also saw something similar to this while upgrading to 49.0.2 internally at InfluxData. I did some basic profiling and I have some ideas on how to make this faster
Methodology
I looked at this with XCode instruments (and didn't let it run to completion)
I also ran samply like this on a smaller example (50 cols)
Analysis
I calculated the breakdown like this
And then I dug into the different users. One interesting pattern that emerges is that they all have very deep callstacks. For example, in planning, there is a very deep callstack for `datafusion_sql::set_expr::::set_expr_to_plan::{{closure}}. In rewrite with subqueries, there is also a very deep callstack.
I think this comes from the fact that a query like
Is planned like
I didn't find any one callstack with a massive smoking gun
Thus, I hypothesize there is some non trivial overhead due to the number of nodes / depth of plan (due to the rewriting that occurs). So one potential idea to try is to unnest unions earlier in the plan (maybe even directly in the planner) which would reduce both the number of nodes in the plan as well as the depth
Note that @crepererum implemented a version of this rewrite for influxdb_iox (though it is in the physical optimizer).
https://github.com/influxdata/influxdb3_core/blob/d14943e575e8a4692aa915ae3fab016712e6e3b3/iox_query/src/physical_optimizer/union/nested_union.rs#L11
Omega359 commentedon Sep 9, 2025
My short term solution was to split the dag in two and do two separate calls writing out and reading back the results in between. That cut the number of items in each of the logical plans down significantly and thus planning time down tremendously. It's not an ideal solution but it worked.
xudong963 commentedon Sep 10, 2025
FYI, our team is also verifying the "Planning time for queries with many columns" issue, will give some feedback later.
alamb commentedon Sep 10, 2025
To be clear, I am suggesting instead of:
To make the plan like this (Union already supports more than one input)
logan-keede commentedon Nov 7, 2025
This is the result I get by using
EXPLAIN ANALYZEwithctx.sqlwherectxis an instance ofSessionContext, on the other hand using datafusion-cli I get:-Which is much cleaner and I believe that's what @alamb is referring to in the above comment.
Is this difference between
ctx.sqlanddatafusion-cliexpected?It seems like benchmarks is skipping over some optimizations somehow.
Perhaps making sure that benchmarks and datafusion-cli execute queries in the same way will be enough to solve this issue. WDYT?
alamb commentedon Nov 7, 2025
Making sure the benchmark is doing the same thing as users (and datafusion-cli) sounds like a very wise first step to me
logan-keede commentedon Nov 7, 2025
After some testing to narrow the cause of the difference between benchmark and datafusion-cli, I found out that the cause is the fact that
UInt64somehow does not get optimised byeliminate_nested_unionoptimiser(which is being used in benchmarks), after changing it toInt64it seems to work fine.It reduces the time by about half for 50 columns and 100 columns scenario.
logan-keede commentedon Nov 9, 2025
For UInt64 scenario logical-plan optimizer is unable to optimize/detect nested union because each union is wrapped up in a projection like-
This is not the case for other scenarios (tested for UInt32, Int64), so they are optimized.
Since, this is an edge case. I think we should temporarily shift the benchmark to Int64, while we raise a separate bug ticket or (change the description of this one).
alamb commentedon Nov 9, 2025
This makes a lot of sense to me -- thank you @logan-keede
So your proposal is to file a ticket for supporting nested_unions when there is a projection between them? (this makes a lot of sense to me and I am hopeful that it would improve performance substantially)
logan-keede commentedon Nov 9, 2025
Yes, I can make a PR to shift the benchmark to Int64 format, if that seems fine to you.
Omega359 commentedon Nov 9, 2025
So ... instead of fixing the issue we're just going to modify the benchmark to ignore the problem?
Err, nm, I just reread previous comments. I still think a benchmark showcasing the issue should be left in though.
logan-keede commentedon Nov 9, 2025
what do you suggest? My understanding is that the only benchmark showcasing this is the worst case scenario, which does not showcase the improvement or regression over the general case, thus creating a blind spot.
I am not saying that we ignore this case, and their maybe other cases too that suffer the same problem.
I just thought that having a ticket tracking the issue would be enough, maybe we can have multiple versions of the same benchmark for different Datatypes?
Omega359 commentedon Nov 10, 2025
While this may be the worst case scenario it is also my general case unfortunately. I perhaps selfishly would like the benchmark to still showcase that there is a lot of room for improvement here, especially once you increase the column count to 200+ columns.
That being said I want to say that I really do appreciate your investigation and isolation of one of the causes of the performance issue here. I am completely fine with potentially adjusting the benchmark to showcase the difference in performance among various datatypes - it shouldn't be difficult to do that. If you want I can do that early this week assuming nothing else comes up at work.
1 remaining item
Separating Benchmarks for physical sorted union over large columns in…
logan-keede commentedon Nov 12, 2025
After some more sleuthing, I think I have narrowed down on the cause of this problem
Consider the following query and its unoptimized Logical Plan:-
Here,
0which is a reference to the tables column number is interpreted as an literalInt64(0)before being referred.TypeCoercionRewriterusing coerce_union:-datafusion/datafusion/optimizer/src/analyzer/type_coercion.rs
Lines 216 to 243 in cf03574
Which tries to coerce union's Input into same time. It sees that it has an Int64 literal as an argument and other UInt64 columns and tries to coerce it using coercion Rules mentioned here:-
datafusion/datafusion/expr-common/src/type_coercion/binary.rs
Lines 1293 to 1299 in cf03574
Which results in a projection layer being added on top of union to coerce its output
something like:-
Now, union does not have its descendant unions as direct children so,
EliminateNestedUnionfails to eliminate this Union.After I remove above mentioned two coercion rules(2nd one also tries to coerce UInt64, and adds a projection layer)
I get an optimized logical plan like the Int64 datatype scenario.
Proposed Solution:-
Stop
TypeCoercercionRewriterfrom trying to coerce on the basis of column reference's inferred type.Omega359 commentedon Nov 12, 2025
If you have a branch for this somewhere I'd love to run some tests with it.
logan-keede commentedon Nov 12, 2025
You can use this branch: - https://github.com/logan-keede/datafusion/tree/experimental_union_planning
Please note that I am not proposing removing coercion rules as a solution. I don't have the branch with a solution yet this is just for the experimental stuff I did to reach the above-mentioned conclusions.
alamb commentedon Nov 13, 2025
This is great 🕵️ @logan-keede 🙏
One potential way to improve this would be to teach the optimizer to combine the Projections somehow -- specifically I think the current https://github.com/apache/datafusion/blob/967384e16257bab4deb48bbeff38507ea13c4abd/datafusion/optimizer/src/eliminate_nested_union.rs#L39-L38 rewrites this pattern:
Into this pattern:
Maybe we could extend the code somehow to rewrite this pattern too:
(aka "look through" the projection)
Actually now that I type this, it might hold for any number of single input children between the Union and their children (not just for Projections)
Consolidate `EliminateNestedUnion` and `EliminateOneUnion` optimizer …
Consolidate `EliminateNestedUnion` and `EliminateOneUnion` optimizer …
Optimize planning for projected nested union (#18713)
Omega359 commentedon Jan 19, 2026
I ran samply last night against a slightly modified sql_planner_extended benchmark which had some interesting results.
I'm going to add logging for timings per optimizer shortly and retest.
alamb commentedon Jan 22, 2026
that is quite fasinating (I have not seen PredicateBoundsEvaluator before -- I wonder if that is something that got newly added)
Note that @askalt also has a PR that improves some of the deeply nested union planning benchmarks by 50%
PlanProperties, add fast-path forwith_new_children#19792Omega359 commentedon Jan 22, 2026
I just saw that PR earlier today @alamb - I am going to see if I can redo the test this weekend with that PR in place to see how much if affects things.
Omega359 commentedon Jan 25, 2026
Unless there is a flag that is required the changes in #19792 seemed to have had no impact on the sql_planner_extended benchmark as modified above.
dexhunter commentedon Jul 29, 2026
take
perf: avoid quadratic metadata merging in union schemas (apache#26790)