Skip to content

rfc: distributed dynamic filtering - #553

Open
jayshrivastava wants to merge 17 commits into
mainfrom
js/dynamic-filtering-design
Open

rfc: distributed dynamic filtering#553
jayshrivastava wants to merge 17 commits into
mainfrom
js/dynamic-filtering-design

Conversation

@jayshrivastava

@jayshrivastava jayshrivastava commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

introduces the proto converter to the PhysicalExtensionCodec trait
which helps dedupe dynamic filters
@jayshrivastava
jayshrivastava changed the base branch from main to branch-55 July 14, 2026 19:12
@jayshrivastava jayshrivastava changed the title Js/dynamic filtering design rfc: distributed dynamic filtering Jul 14, 2026
Comment thread rfcs/1-distributed-dynamic-filtering.md
@jayshrivastava jayshrivastava changed the title rfc: distributed dynamic filtering [WIP] rfc: distributed dynamic filtering Jul 20, 2026
@jayshrivastava
jayshrivastava marked this pull request as ready for review July 22, 2026 19:49
@jayshrivastava jayshrivastava changed the title [WIP] rfc: distributed dynamic filtering rfc: distributed dynamic filtering Jul 22, 2026

@gabotechs gabotechs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is looking very good! I really do not have any major comments, this seems to be going in the right direction.

Comment on lines +268 to +272
The proposed change is to implement dynamic filtering similarly to Trino.

1. During execution, the dynamic filter producers (`HashJoinExec`, `SortExec`, `AggregateExec`) will send dynamic filter updates to the coordinator.
2. The coordinator will union / merge the dynamic filter updates from all workers.
3. The coordinator will send the unioned / merged dynamic filter updates to the consumers (`DataSourceExec`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍 Sounds good, with the current infrastructure, I don't think it would be terrible difficult to do this

- Less overhead than ORing

Cons:
- Is brittle. What if we have to support non-range and non-IN-LIST expressions? It would be nice if dynamic filters in vanilla datafusion natively implemented a `merge` or `union` operation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do you think it's brittle? For merging, I imagine you can just rely on Arrow comparison kernels for deduping IN LIST expressions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can you expand on this? It's easy to implement a merge operation if we assume the expressions to merge always look like a@0 >= 0 AND a@0 <= 5 OR IN LIST [10, 11]. We would downcast to InListExpr and RangeExpr and define a merge operation for those.

In the future, what if there's other kinds of expressions in there? Then maybe the merge operation would break.

Maybe @LiaCastaneda has more insights on this. I think she implemented something similar.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I imagine that not any arbitrary expression can be placed there. Even if it can, you still decide to optimize the most common cases, like IN LISTs or min/max ranges, and if there's anything else besides does two things just leave it OR-ed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In the future, what if there's other kinds of expressions in there?

I think that rather than adding other kinds of expressions, there is probably a higher possibility that the overall dynamic filter shape changes from the current CASE statement to something else.
Regarding expression types, I think the other discussed expression that hasn’t been implemented yet is bloom filters pushdown as dynamic filters. In that case, deduping probably becomes awkward, I’m not really sure how you would merge that (or if its possible). But I think it’s a very niche use case, and as Gabriel said, it can be ORed anyway.


// Read Updates
pub fn current(&self) -> Result<Arc<dyn PhysicalExpr>>
pub(crate) fn subscribe(&self) -> DynamicFilterSubscription

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤔 subscribe not being public might be a problem.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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


#### Options 1 & 2: Support new APIs In Vanilla DataFusion

See https://github.com/apache/datafusion/issues/23814

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

apache/datafusion#23814 seems to be evolving in a good direction, and people seem engaged in the discussion, so I'd lean towards using whatever comes out of that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yup I will stay on top of that issue.

Comment thread rfcs/1-distributed-dynamic-filtering.md Outdated
Comment on lines +274 to +275
For correctness, the coordinator must wait for all dynamic filter updates from all workers before sending them to the consumers, otherwise, the
consumers may prune rows incorrectly.

@barbarj barbarj Aug 6, 2026

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.

Is this
A) the coordinator must get an update from each worker at least once before sending the updated expression, and then can update eagerly going forward
B) the coordinator must get an update from each worker at least once per filter generation before sending the next generation?
C) or something else?

Relatedly, this is overly strict for SortExec's TopK dynamic filter, as well as AggregateExec's min/max filter, which can safely be updated eagerly. (I've no problem with correctness then optimization as the sequence for shipping this though)

@jayshrivastava jayshrivastava Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great point. For hash joins, eager updates aren't safe. The filter effectively contains the build side of the join. If the join is on different workers and each has a different build side, we need filter ORed together to get a global filter which represents the entire build side across workers.

However for sort and aggregate, I think eager updates are very safe. For an aggregate that has MIN(a) for example the aggregate on any worker can push down a < 10 for example. We can OR the expressions across workers ex. a < 10 OR a < 20 and the minimum value, 10 would win. This mechanism is similar for SortExec.

For now, I think we should ensure on correctness / safety in the initial implementation, so I'm proposing option A) for now. I think we can roll out eagerness later. This will mean the dynamic filters on SortExec and AggregateExec will still be pretty useless for now, but we will be able to unlock joins.

Since you worked on joins recently, maybe you can confirm if what I said makes sense

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.

Yeah, after reading up a bit more on HashJoinExec's filter strategy, that makes sense, and A) is a reasonable general approach.

We'll want to consider how we can track each expression's producer node's type, as that will be necessary info for enabling eagerness correctly later.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We'll want to consider how we can track each expression's producer node's type, as that will be necessary info for enabling eagerness correctly later.

Agreed. At least, maybe some property to indicate if its safe to apply eager updates or not.

I pushed a short section in the RFC on eager updates so this is documented.

pull Bot pushed a commit to Stars1233/datafusion that referenced this pull request Aug 10, 2026
…he#22437) (apache#24018)

## Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes #123` indicates that this PR will close issue #123.
-->

- Informs: apache#23814
- Informs:
datafusion-contrib/datafusion-distributed#584

This change does not close the above issues because it does not
implement a way to tell if a node is a producer dynamic filters.

## Rationale for this change

See apache#23814 and
datafusion-contrib/datafusion-distributed#553.

To send dynamic filter updates across the network, there needs to be a
way to get access to `PhysicalExpr` from `ExecutionPlan`. As discussed
in apache#23814, the cleanest way
to do this is to add `ExecutionPlan::apply_expressions`, which mirrors a
similar method for logical plan nodes.

## What changes are included in this PR?

There's 3 commits in this PR:

Firstly, commit 1 re-applies the changes in
apache#20337 (reverted in
apache#22437).

Some of the reasons for why the original PR was reverted include
(a) `apply_expressions` is too complicated to implement and there's no
concrete need to justify this complexity
(b) there was no usage of `apply_expressions` inside this repo

To address (a)
- justification for adding this method is provided in
apache#23814
- commit 2 in this PR adds helper methods `apply_expression_roots` and
`apply_no_expressions` which abstract away the `TreeNodeRecursion`
complexity from implementors. Now, `apply_expressions` very trivial to
implement ex.
```rust
fn apply_expressions(
    &self,
    f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
    apply_expression_roots([&self.predicate_1], f)
    apply_expression_roots([&self.predicate_2], f)
    apply_expression_roots([&self.other_expression], f)
}
```
- the method traverses over `&Arc<dyn PhysicalExpr>` rather than `&dyn
PhysicalExpr` to reduce complexity around lifetimes

To address (b):
- commit 3 adds a usage of `apply_expressions` in
`physical-plan/src/aggregates/mod.rs`. Previously, there was a hack that
checked if a filter was pushed down using `Arc::strong_count(dyn_filter)
> 1`. Now it uses `apply_expressions`
- similarly, commit 4 removes `is_used` from dynamic filters which used
to check Arc references counts to see if a filter was pushed down. Now,
the hash join uses `apply_expressions` to find pushed down filters.

## Are these changes tested?

Yes.

## Are there any user-facing changes?

There's a new mandatory method `ExecutionPlan::apply_expressions()`. See
the upgrading guide and documentation for details.

---------

Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>

@shinzoxD shinzoxD left a comment

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.

The new eager-update section leaves one liveness boundary to clarify.

#### Sorts
These operate similarly to Aggregates and can be applied eagerly.

The proposal in this RFC is to take the safest approach and wait for all filters to complete before

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.

The preceding paragraph says aggregate filters are never marked complete, so a literal wait for all discovered filters can never finish for a plan containing AggregateExec (and presumably SortExec). Could the initial scope explicitly define the required-completion set as join-produced filters only, and state that filter collection must not block query execution? Otherwise an implementation that registers every producer can either never publish these filters or deadlock while awaiting completion. Non-completing producers can then be added when eager updates land.

kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…he#22437) (apache#24018)

## Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes apache#123` indicates that this PR will close issue apache#123.
-->

- Informs: apache#23814
- Informs:
datafusion-contrib/datafusion-distributed#584

This change does not close the above issues because it does not
implement a way to tell if a node is a producer dynamic filters.

## Rationale for this change

See apache#23814 and
datafusion-contrib/datafusion-distributed#553.

To send dynamic filter updates across the network, there needs to be a
way to get access to `PhysicalExpr` from `ExecutionPlan`. As discussed
in apache#23814, the cleanest way
to do this is to add `ExecutionPlan::apply_expressions`, which mirrors a
similar method for logical plan nodes.

## What changes are included in this PR?

There's 3 commits in this PR:

Firstly, commit 1 re-applies the changes in
apache#20337 (reverted in
apache#22437).

Some of the reasons for why the original PR was reverted include
(a) `apply_expressions` is too complicated to implement and there's no
concrete need to justify this complexity
(b) there was no usage of `apply_expressions` inside this repo

To address (a)
- justification for adding this method is provided in
apache#23814
- commit 2 in this PR adds helper methods `apply_expression_roots` and
`apply_no_expressions` which abstract away the `TreeNodeRecursion`
complexity from implementors. Now, `apply_expressions` very trivial to
implement ex.
```rust
fn apply_expressions(
    &self,
    f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
    apply_expression_roots([&self.predicate_1], f)
    apply_expression_roots([&self.predicate_2], f)
    apply_expression_roots([&self.other_expression], f)
}
```
- the method traverses over `&Arc<dyn PhysicalExpr>` rather than `&dyn
PhysicalExpr` to reduce complexity around lifetimes

To address (b):
- commit 3 adds a usage of `apply_expressions` in
`physical-plan/src/aggregates/mod.rs`. Previously, there was a hack that
checked if a filter was pushed down using `Arc::strong_count(dyn_filter)
> 1`. Now it uses `apply_expressions`
- similarly, commit 4 removes `is_used` from dynamic filters which used
to check Arc references counts to see if a filter was pushed down. Now,
the hash join uses `apply_expressions` to find pushed down filters.

## Are these changes tested?

Yes.

## Are there any user-facing changes?

There's a new mandatory method `ExecutionPlan::apply_expressions()`. See
the upgrading guide and documentation for details.

---------

Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
@jayshrivastava
jayshrivastava force-pushed the branch-55 branch 2 times, most recently from 78b65f1 to b8eddac Compare August 12, 2026 17:08
@jayshrivastava
jayshrivastava force-pushed the branch-55 branch 3 times, most recently from 300b5af to e114458 Compare August 19, 2026 21:41
Base automatically changed from branch-55 to main August 20, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants