Repository navigation
Support dynamic filters and work-table plans state reset #19905
Copy link
Copy link
Open
Description
There is a reset_plan_states function which is intended to re-use an existing ExecutionPlan:
datafusion/datafusion/physical-plan/src/execution_plan.rs
Lines 1401 to 1423 in d90d074
| /// Make plan ready to be re-executed returning its clone with state reset for all nodes. | |
| /// | |
| /// Some plans will change their internal states after execution, making them unable to be executed again. | |
| /// This function uses [`ExecutionPlan::reset_state`] to reset any internal state within the plan. | |
| /// | |
| /// An example is `CrossJoinExec`, which loads the left table into memory and stores it in the plan. | |
| /// However, if the data of the left table is derived from the work table, it will become outdated | |
| /// as the work table changes. When the next iteration executes this plan again, we must clear the left table. | |
| /// | |
| /// # Limitations | |
| /// | |
| /// While this function enables plan reuse, it does not allow the same plan to be executed if it (OR): | |
| /// | |
| /// * uses dynamic filters, | |
| /// * represents a recursive query. | |
| /// | |
| pub fn reset_plan_states(plan: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> { | |
| plan.transform_up(|plan| { | |
| let new_plan = Arc::clone(&plan).reset_state()?; | |
| Ok(Transformed::yes(new_plan)) | |
| }) | |
| .data() | |
| } |
Currently, it does not support dynamic filters and work table of the recursive query, because they are injected into nodes once on the planning stage and then never updated. Actually, to re-use a plan, dynamic filters must be reset.
Possible implementation
- Add
ExecutionPlan::dynamic_filters(...)method which is intended to return current version of dynamic filters. - Introduce
PushedDynamicFiltersstruct to pass intoExecutionPlan::with_new_statemethod. - Modify
reset_plan_statesto use the following algorithm:- Create an empty
PushedDynamicFilters - Create an option
work_table(Noneinitially) - Transform plan up to bottom
- When process a node: reset its state, use
with_new_state(pushed_filters)andwith_new_state(work_table)ifwork_tableis Some. Then collect new dynamic filters from it and possibly a work table if it is aRecursiveQueryExec(successfull downcast).
- Create an empty
- For each node that polls dynamic filter support
with_new_statewhere will be an attempt to down-cast state intoPushedDynamicFilters.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
askalt commentedon Jan 20, 2026
take
jayzhan211 commentedon Oct 16, 2026
@askalt are you still working on this? I looked at the same area and would like to suggest a simpler model, based on what main does today.
What happens today in a recursive CTE (the recursive term is reset with
reset_statebefore each iteration). The three dynamic-filter producers each do something different:reset_stateAggregateExec(MIN/MAX)HashJoinExechash_join/exec.rs,dynamic_filter = None)SortExec(TopK)sort.rs,create_filter())Repro for the hash join (main, 81824e8):
This matters for graph walks, the common use of recursive CTEs. On a 20M-row edge table sorted by
src, one join step takes 3 ms with the dynamic filter (19 of 20 row groups pruned) and 24 ms without it; a 10-step recursive walk takes 0.90 s today.Suggested model: a producer owns one filter and resets its value, never its identity. The scan already holds a reference to that filter from planning. If
reset_stateclears the value in place instead of dropping or replacing the filter, the scan sees the next iteration's values with no re-wiring, so thedynamic_filters()/PushedDynamicFiltersplumbing proposed above would not be needed.#26056 already does this for
AggregateExecand addsDynamicFilterPhysicalExpr::reset(). After it lands, the remaining steps are small:HashJoinExec::reset_statekeeps its filter and callsreset()instead of setting it toNone, plus acte.sltcase that checks pruning in a recursive join.SortExec::reset_statedoes the same instead ofcreate_filter(), plus acte.sltcase for TopK.If you are no longer working on this, would it be OK to mark it help wanted with these steps?
adriangb commentedon Oct 17, 2026
This makes sense to me @jayzhan211
askalt commentedon Oct 19, 2026
The initial intention was to give an ability to reuse an existing physical plan. This model allows to do it, however each
.execute(...)must be called strictly afterreset_plan_stateand, hence, after the previous stream is exhausted. To execute the same query in parallel, we need to create a physical plan N times for it. It seems acceptable to me. It would be cool to reflect this in a doc.Yes, I am definitely won't be able to work on this issue the next 2 weeks, so it is OK to mark it help wanted.