Skip to content

Conversation

@askalt
Copy link
Contributor

@askalt askalt commented Jan 20, 2026

Which issue does this PR close?

Rationale for this change

Improve performance of query planning and plan state re-set by making node clone cheap.

What changes are included in this PR?

  • Store projection as Option<Arc<[usize]>> instead of Option<Vec<usize>> in FilterExec, HashJoinExec, NestedLoopJoinExec.
  • Store exprs as Arc<[ProjectionExpr]> instead of Vec in ProjectionExprs.
  • Store arced aggregation, filter, group by expressions within AggregateExec.

@github-actions github-actions bot added physical-expr Changes to the physical-expr crates optimizer Optimizer rules core Core DataFusion crate common Related to common crate proto Related to proto crate physical-plan Changes to the physical-plan crate labels Jan 20, 2026
@askalt askalt force-pushed the askalt/arc_immutable branch from b9caf1d to bb97763 Compare January 20, 2026 08:17
@askalt
Copy link
Contributor Author

askalt commented Jan 20, 2026

run benchmark reset_plan_states

@alamb-ghbot
Copy link

🤖 Hi @askalt, thanks for the request (#19893 (comment)). scrape_comments.py only responds to whitelisted users. Allowed users: Dandandan, Omega359, adriangb, alamb, comphead, gabotechs, geoffreyclaude, klion26, rluvaton, xudong963, zhuqi-lucas.

@askalt
Copy link
Contributor Author

askalt commented Jan 20, 2026

@xudong963 could you please run benchmark reset_plan_states?

@xudong963
Copy link
Member

run benchmark reset_plan_states

@alamb-ghbot
Copy link

🤖 ./gh_compare_branch_bench.sh compare_branch_bench.sh Running
Linux aal-dev 6.14.0-1018-gcp #19~24.04.1-Ubuntu SMP Wed Sep 24 23:23:09 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing askalt/arc_immutable (bb97763) to d90d074 diff
BENCH_NAME=reset_plan_states
BENCH_COMMAND=cargo bench --features=parquet --bench reset_plan_states
BENCH_FILTER=
BENCH_BRANCH_NAME=askalt_arc_immutable
Results will be posted here when complete

@alamb-ghbot
Copy link

🤖: Benchmark completed

Details

group     askalt_arc_immutable                   main
-----     --------------------                   ----
query1    1.00     32.2±1.09ms        ? ?/sec    1.01     32.4±0.31ms        ? ?/sec
query2    1.00      9.2±0.08ms        ? ?/sec    1.03      9.5±0.11ms        ? ?/sec
query3    1.00     12.2±0.25ms        ? ?/sec    1.15     14.1±0.08ms        ? ?/sec

@askalt askalt force-pushed the askalt/arc_immutable branch 2 times, most recently from 70a396a to 3614687 Compare January 21, 2026 10:04
@github-actions github-actions bot added datasource Changes to the datasource crate and removed optimizer Optimizer rules core Core DataFusion crate common Related to common crate proto Related to proto crate labels Jan 21, 2026
@askalt askalt force-pushed the askalt/arc_immutable branch from 3614687 to bb97763 Compare January 21, 2026 10:08
@github-actions github-actions bot added optimizer Optimizer rules core Core DataFusion crate common Related to common crate proto Related to proto crate and removed datasource Changes to the datasource crate labels Jan 21, 2026
@askalt askalt force-pushed the askalt/arc_immutable branch from bb97763 to f2d829a Compare January 21, 2026 10:11
@askalt askalt force-pushed the askalt/arc_immutable branch from f2d829a to 050e6bb Compare January 21, 2026 12:39
@crepererum
Copy link
Contributor

I understand that we wanna avoid breaking changes and hence are kinda liberal with impl Into<...>/impl AsRef<...> parameters. However these methods are generic and will be compiled on the call-site, i.e. whoever calls these methods has to recompile them. Furthermore calling these methods with different types duplicates these methods in LLVM and likely also in the resulting binary. This means: longer compile times and binary size bloat.

Hence I would favor if we could avoid these impl parameters.

@askalt
Copy link
Contributor Author

askalt commented Jan 22, 2026

I understand that we wanna avoid breaking changes and hence are kinda liberal with impl Into<...>/impl AsRef<...> parameters. However these methods are generic and will be compiled on the call-site, i.e. whoever calls these methods has to recompile them. Furthermore calling these methods with different types duplicates these methods in LLVM and likely also in the resulting binary. This means: longer compile times and binary size bloat.

Hence I would favor if we could avoid these impl parameters.

Actually, I also think that using a generic here isn't justified, since &Vec<u8> can always be converted to a slice. I believe the API should be as simple as possible. Let's get @alamb opinion.

@alamb
Copy link
Contributor

alamb commented Jan 23, 2026

I understand that we wanna avoid breaking changes and hence are kinda liberal with impl Into<...>/impl AsRef<...> parameters. However these methods are generic and will be compiled on the call-site, i.e. whoever calls these methods has to recompile them. Furthermore calling these methods with different types duplicates these methods in LLVM and likely also in the resulting binary. This means: longer compile times and binary size bloat.
Hence I would favor if we could avoid these impl parameters.

Actually, I also think that using a generic here isn't justified, since &Vec<u8> can always be converted to a slice. I believe the API should be as simple as possible. Let's get @alamb opinion.

Indeed my concern is the impact on (all) downstream users during the upgrade. I believe the initial PR from @askalt was substantially larger because it required changing a bunch of callsites.

If we can avoid the use of impl and not require many changes (I understand some may be impossible to avoid) that sounds good to me

@alamb alamb added the api change Changes the API exposed to users of the crate label Jan 23, 2026
Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

Thank you for this PR @askalt and for the review @crepererum and @martin-g

I think the performance results are quite compelling and this is quite close.

I agree it would be find to avoid the AsRef impls, if you are willing to do so @askalt . I also think it would be fine to leave this as is.

In any event, I think we should add an example to project_schema that shows how to call it with Option<&Vec<usize>> (what is passed by TableProvider::scan)

Before we merge this PR I think we

  1. should also add a note to the upgrade guide with a note about this change (I can help with this, but I didn't want to push changes to this PR before we have consensus approach)
  2. Clean up the API for OptionProjectionRef (move it, and make Option<ProjectionRef>)

pub fn project_schema(
schema: &SchemaRef,
projection: Option<&Vec<usize>>,
projection: Option<&impl AsRef<[usize]>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Per @crepererum 's suggestion, I tried out what it would look like and came up with

It does seem to be reasonable

The original signature is Option<&Vec<..>> I think to align with TableProvider::scan (which also shouldn't have a owned Vec, but that I think is a historic accident)

mode: AggregateMode,
/// Group by expressions
group_by: PhysicalGroupBy,
group_by: Arc<PhysicalGroupBy>,
Copy link
Contributor

Choose a reason for hiding this comment

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

likewise here I think it would be good to note they are Arc to make clone/plan rewriting faster

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// Returns an internal error if existing projection contains index that is
/// greater than len of the passed `projection`.
///
pub fn apply_projection<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this is basically refactored out of NestedLoops join (and maybe elsewhere)

}
}

/// Describes an option immutable reference counted shared projection.
Copy link
Contributor

Choose a reason for hiding this comment

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

I see why you need something like this (as it changes the signture of TableProvider) but this seems inconsistent with the other representations of projections in DataFusion/Arrow I am familiar with, which are represented with an Option rather than having the Option internally

I recommend:

  1. Move this into datafusion/physical-expr/src/projection.rs (not in physical plan) so it is near ProjectionExprs
  2. Move the Option outside (so the signature is Option<ProjectionRef> rather than OptionProjectionRef)

Copy link
Contributor

Choose a reason for hiding this comment

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

I also recommend documenting why this structure exists (because it is cheap to clone)

Something like

/// This structure represents projecting a set of columns by index.
/// It uses an `Arc` internally to make it cheap to clone

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Move this into datafusion/physical-expr/src/projection.rs (not in physical plan) so it is near ProjectionExprs

Done

I also recommend documenting why this structure exists (because it is cheap to clone)

Done

  1. Move the Option outside (so the signature is Option rather than OptionProjectionRef)

Initially I done in this way, but in this case, take a look at possible HashJoinExec::try_new signature. It seems we want something like

fn try_new(
    ...
   projection: Option<impl Into<ProjectionRef>>,
   ...
)

To be able to pass Some(vec![1,2,3]) and Some(projection_ref) as well. But Option<impl ...> forces us to explicitly specify None type, like None::<ProjectionRef> which looks annoying for me. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we should try it -- #19893 (review)

Specotying the None type does look annoying -- maybe we could make a version of the function that takes no projecton -- like try_new_without_projection 🤔

@crepererum
Copy link
Contributor

A slightly more verbose trick to avoid the code bloat but still get the impl Into<...>/impl AsRef<...> would be to split the respective methods into a public and private part. Using an impl Into<...> example, then something like:

pub fn foo(&self, param: String) -> ... {
   ...
}

would become

pub fn foo(&self, param: impl Into<String>) -> ... {
    self.foo_impl(param.into())
}

fn foo_impl(&self, param: String) -> ... {
    // that's just the "old" `foo` method
    ...
}

i.e. you basically control the generic part a bit more manual. That was what once was planned (but no longer is) as an automatic step, see rust-lang/rust#124962

@askalt askalt force-pushed the askalt/arc_immutable branch from 050e6bb to 20b3042 Compare January 26, 2026 20:41
@github-actions github-actions bot added documentation Improvements or additions to documentation catalog Related to the catalog crate datasource Changes to the datasource crate labels Jan 26, 2026
@askalt askalt force-pushed the askalt/arc_immutable branch from 20b3042 to e6fed67 Compare January 26, 2026 20:45
@askalt
Copy link
Contributor Author

askalt commented Jan 26, 2026

I decided to simplify Statistics::project and project_schema signature and take an Option slice. Added a note to upgrading.md. Anyway, @crepererum thank you for the mentioned trick!

@askalt askalt force-pushed the askalt/arc_immutable branch 2 times, most recently from 5768265 to 1e27378 Compare January 26, 2026 20:51
- Closes apache#19852

Improve performance of query planning and plan state re-set by making node clone cheap.

- Store projection as `Option<Arc<[usize]>>` instead of `Option<Vec<usize>>` in `FilterExec`,
  `HashJoinExec`, `NestedLoopJoinExec`.
- Store exprs as `Arc<[ProjectionExpr]>` instead of Vec in `ProjectionExprs`.
- Store arced aggregation, filter, group by expressions within `AggregateExec`.
Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

I am sorry this is taking so long, but I a very worried about API churn and it takes me a non trivial amount of time to review the potential implications of this change

I have two major points:

  1. I really think it is important to not force downstream users to change how they call project_schema as it is a very common operation and there is no technical reason we can't keep their code the same (though it is harder in Datafusion). I made a PR to do so (and backout many of the changes currently required in this PR): askalt#5 as suggested by @crepererum
  2. As I mentioned previously OptionProjectionRef seems overly complicated to me to add to the public API -- and because the Option is wrapped, it means that the normal functions for Option (like match and .and_then, etc) don't work. I think it would be simpler, and follow the rest of the code more closely if it were Option<ProjectionRef> and the special projection application code is put into ProjectionRef

Thank you for your work on this @askalt

@askalt
Copy link
Contributor Author

askalt commented Jan 29, 2026

I am sorry this is taking so long, but I a very worried about API churn and it takes me a non trivial amount of time to review the potential implications of this change

I have two major points:

  1. I really think it is important to not force downstream users to change how they call project_schema as it is a very common operation and there is no technical reason we can't keep their code the same (though it is harder in Datafusion). I made a PR to do so (and backout many of the changes currently required in this PR): askalt#5 as suggested by @crepererum
  2. As I mentioned previously OptionProjectionRef seems overly complicated to me to add to the public API -- and because the Option is wrapped, it means that the normal functions for Option (like match and .and_then, etc) don't work. I think it would be simpler, and follow the rest of the code more closely if it were Option<ProjectionRef> and the special projection application code is put into ProjectionRef

Thank you for your work on this @askalt

Ok, I will do it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate catalog Related to the catalog crate common Related to common crate core Core DataFusion crate datasource Changes to the datasource crate documentation Improvements or additions to documentation optimizer Optimizer rules physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrap more immutable plan parts into Arc

6 participants