pub struct SemiJoinPushdownThroughInnerJoin { /* private fields */ }Expand description
Push an existing semi-join down through an inner join, so the selective side filters one join input instead of the join’s output.
§The query that motivated this
TPC-H q18’s o_orderkey IN (SELECT l_orderkey … HAVING sum(l_quantity) > 300)
decorrelates to a semi-join, and DataFusion leaves it at the very top:
HashJoin [RightSemi] on (l_orderkey, o_orderkey) 300.92 s
Filter: sum(l_quantity) > 300 <- keeps ~570 of 150M orders
Aggregate: groupBy=[l_orderkey]
HashJoin [Inner] on (o_orderkey, l_orderkey) 764.03 s <- all 600M rows
HashJoin [Inner] on (c_custkey, o_custkey) 68.07 sMeasured at SF100 the joins are 82.9% of the query and the aggregate only 16.9%, so this is a join-ordering problem, not an aggregation one. The most selective predicate in the whole query — 570 surviving orders out of 150M — executes last, after the 764 s join has already materialised the full customer/orders/lineitem cross-section.
§The rewrite
For an inner join whose output feeds a semi- or anti-join keyed on columns from only one side:
SemiJoin(Inner(A, B), S) on A.k ==> Inner(SemiJoin(A, S) on A.k, B)
AntiJoin(Inner(A, B), S) on A.k ==> Inner(AntiJoin(A, S) on A.k, B)§Anti joins and residual filters
Both were originally refused — anti joins as needing “their own reasoning”,
and any join carrying a residual filter because it “may reference both
sides”. Between them those two guards made the rule inert on TPC-H q21,
whose EXISTS/NOT EXISTS produce exactly a semi and an anti join, each
carrying l_suppkey <> l_suppkey. q21 was the slowest query in the SF100
sweep at 4309 s against Spark’s 391 s — the largest single loss of the 22 —
with the most selective predicate in the query running above the whole
four-way join.
The reasoning does carry over. For both kinds the existence test is a
function of the filtered row and the probe alone, so a row of Inner(A, B)
passes exactly when its A row passes. The residual is carried down and
remapped at each level (see remap_residual) rather than refused, and
re-attached only where every column it names resolves into the child being
landed on or the probe.
§Why it is safe
- The join below must be Inner. An outer join null-pads its non-preserved side, so a key that is null after the join was not null before it, and filtering earlier would keep different rows.
- Every semi-join key must resolve into one side. If the keys straddle
AandB, the existence test genuinely depends on the joined row and cannot be evaluated before the join. The same test is applied to the residual’s columns. - Row multiplicity is preserved. A semi-join emits each surviving row
at most once and adds no columns, so
Inner(SemiJoin(A,S), B)produces exactly the rows ofInner(A,B)whoseA.khad a match — which is the definition of the original. Counts and sums downstream are unchanged. - The output schema is identical. Semi-joins project only their
filtered side, so
A ⧺ Bin both forms, in the same order.
The outer semi-join is replaced rather than duplicated, so there is no fixed-point concern: after one application the top node is an inner join.
Implementations§
Trait Implementations§
Source§impl Default for SemiJoinPushdownThroughInnerJoin
impl Default for SemiJoinPushdownThroughInnerJoin
Source§fn default() -> SemiJoinPushdownThroughInnerJoin
fn default() -> SemiJoinPushdownThroughInnerJoin
Source§impl OptimizerRule for SemiJoinPushdownThroughInnerJoin
impl OptimizerRule for SemiJoinPushdownThroughInnerJoin
Source§fn apply_order(&self) -> Option<ApplyOrder>
fn apply_order(&self) -> Option<ApplyOrder>
ApplyOrder for details. Read moreSource§fn rewrite(
&self,
plan: LogicalPlan,
_config: &dyn OptimizerConfig,
) -> Result<Transformed<LogicalPlan>>
fn rewrite( &self, plan: LogicalPlan, _config: &dyn OptimizerConfig, ) -> Result<Transformed<LogicalPlan>>
plan to an optimized form, returning Transformed::yes
if the plan was rewritten and Transformed::no if it was not. Read moreSource§fn supports_rewrite(&self) -> bool
fn supports_rewrite(&self) -> bool
This method is no longer used
Auto Trait Implementations§
impl Freeze for SemiJoinPushdownThroughInnerJoin
impl RefUnwindSafe for SemiJoinPushdownThroughInnerJoin
impl Send for SemiJoinPushdownThroughInnerJoin
impl Sync for SemiJoinPushdownThroughInnerJoin
impl Unpin for SemiJoinPushdownThroughInnerJoin
impl UnsafeUnpin for SemiJoinPushdownThroughInnerJoin
impl UnwindSafe for SemiJoinPushdownThroughInnerJoin
Blanket Implementations§
impl<T> Allocation for T
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> FutureExt for T
impl<T> FutureExt for T
Source§fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
Source§fn with_current_context(self) -> WithContext<Self> ⓘ
fn with_current_context(self) -> WithContext<Self> ⓘ
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request