Skip to content

Commit d1369d1

Browse files
committed
Kept null-aware anti-join NULLs in the pushed dynamic filter.
The hash-join dynamic filter pushed `key IN build_keys` down to the probe scan for null-aware anti joins too. That drops the probe-side NULL, but `NOT IN` three-valued logic needs it to collapse the result to zero rows, so the join silently returned rows. OR `probe_key IS NULL` into the pushed predicate. Non-NULL probe rows still get filtered; only the NULL additionally survives.
1 parent ad1a260 commit d1369d1

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

datafusion/physical-plan/src/joins/hash_join/exec.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,7 @@ impl ExecutionPlan for HashJoinExec {
13631363
filter,
13641364
on_right,
13651365
repartition_random_state,
1366+
self.null_aware,
13661367
))
13671368
})))
13681369
})

datafusion/physical-plan/src/joins/hash_join/shared_bounds.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use datafusion_common::{DataFusionError, Result, ScalarValue, SharedResult};
3737
use datafusion_expr::Operator;
3838
use datafusion_functions::core::r#struct as struct_func;
3939
use datafusion_physical_expr::expressions::{
40-
BinaryExpr, CaseExpr, DynamicFilterPhysicalExpr, InListExpr, lit,
40+
BinaryExpr, CaseExpr, DynamicFilterPhysicalExpr, InListExpr, IsNullExpr, lit,
4141
};
4242
use datafusion_physical_expr::{PhysicalExpr, PhysicalExprRef, ScalarFunctionExpr};
4343

@@ -255,6 +255,9 @@ pub(crate) struct SharedBuildAccumulator {
255255
repartition_random_state: SeededRandomState,
256256
/// Schema of the probe (right) side for evaluating filter expressions
257257
probe_schema: Arc<Schema>,
258+
/// Null-aware anti join (`NOT IN`). A probe-side NULL must reach the join so its
259+
/// three-valued logic can collapse the result, so the pushed filter keeps NULL rows.
260+
null_aware: bool,
258261
}
259262

260263
/// Strategy for filter pushdown (decided at collection time)
@@ -358,6 +361,7 @@ impl SharedBuildAccumulator {
358361
dynamic_filter: Arc<DynamicFilterPhysicalExpr>,
359362
on_right: Vec<PhysicalExprRef>,
360363
repartition_random_state: SeededRandomState,
364+
null_aware: bool,
361365
) -> Self {
362366
// Troubleshooting: If partition counts are incorrect, verify this logic matches
363367
// the actual execution pattern in collect_build_side()
@@ -404,6 +408,7 @@ impl SharedBuildAccumulator {
404408
on_right,
405409
repartition_random_state,
406410
probe_schema: right_child.schema(),
411+
null_aware,
407412
}
408413
}
409414

@@ -579,7 +584,8 @@ impl SharedBuildAccumulator {
579584
if let Some(filter_expr) =
580585
combine_membership_and_bounds(membership_expr, bounds_expr)
581586
{
582-
self.dynamic_filter.update(filter_expr)?;
587+
self.dynamic_filter
588+
.update(self.null_aware_filter(filter_expr))?;
583589
}
584590
}
585591
PartitionStatus::Pending => {
@@ -685,12 +691,35 @@ impl SharedBuildAccumulator {
685691
)?) as Arc<dyn PhysicalExpr>
686692
};
687693

688-
self.dynamic_filter.update(filter_expr)?;
694+
self.dynamic_filter
695+
.update(self.null_aware_filter(filter_expr))?;
689696
}
690697
}
691698

692699
Ok(())
693700
}
701+
702+
/// Wraps a pushdown filter so a null-aware anti join keeps its probe-side NULL rows.
703+
///
704+
/// The build-side predicate drops probe rows whose key is NULL, but `NOT IN` three-valued
705+
/// logic needs that NULL to reach the join. OR-ing `probe_key IS NULL` preserves the dynamic
706+
/// filter's selectivity for non-NULL rows while letting the NULL through.
707+
fn null_aware_filter(
708+
&self,
709+
filter_expr: Arc<dyn PhysicalExpr>,
710+
) -> Arc<dyn PhysicalExpr> {
711+
if !self.null_aware {
712+
return filter_expr;
713+
}
714+
// A null-aware anti join is validated to a single probe key.
715+
let probe_key_is_null: Arc<dyn PhysicalExpr> =
716+
Arc::new(IsNullExpr::new(Arc::clone(&self.on_right[0])));
717+
Arc::new(BinaryExpr::new(
718+
filter_expr,
719+
Operator::Or,
720+
probe_key_is_null,
721+
))
722+
}
694723
}
695724

696725
impl fmt::Debug for SharedBuildAccumulator {

0 commit comments

Comments
 (0)