Class OuterJoinDemotionPass
JOIN-004) — turning an outer join into a less outer one
when a filter above it makes the padded rows unreachable.
σ p (A ⟕ B) ≡ σ p (A ⨝θ B) when p rejects NULLs on a column of B
An outer join emits, besides the matched rows, rows padded with NULLs on the side
that had no match. If a predicate above the join can never be true of a row
whose column c is NULL, and c comes from the padded side, then those
padded rows are all filtered out — so producing them was work for nothing, and the
join is equivalent to an inner one.
Why it is worth a rule of its own
Demotion is a gate in front of three optimizations an outer join simply cannot have:
- σ pushdown into both sides.
SelectionPushdownPasspushes into⟕'s left only and⟖'s right only; an inner join takes both. - Merge eligibility and SQL pushdown.
Planner.mergeEligibleis{INNER, SEMI, ANTI}, so an outer join can never take a sort-merge plan, andSqlPushdownPlannerfolds only theta joins into aJOIN … ON. EQ-001. It reads facts out of inner-join conditions only, so a demoted join becomes eligible for equality propagation — which is why this rule runs before it in the pushdown phase.
The planner additionally builds the cheaper side of an inner join by cost
(Planner.buildSide), a choice it does not make for the asymmetric join kinds.
What each shape demotes to
A ⟗ loses one half at a time, because each half is killed by a predicate on
the other side's columns — the unmatched-left rows are the ones carrying NULL
right columns:
| Join | rejects on left columns | rejects on right columns |
|---|---|---|
⟕ | — | ⨝θ |
⟖ | ⨝θ | — |
⟗ | ⟕ | ⟖ |
A ⟗ whose filter rejects on both sides loses both halves at once
and goes straight to ⨝θ — one step, one record.
Null rejection
A predicate rejects NULL on c when it can never evaluate to true
for a row whose c is NULL:
- a comparison, a
LIKE, or an∈whose operand isc— rejects; c IS NOT NULL— rejects;c IS NULL— does not, and that is the trap:σ B.x IS NULL (A ⟕ B)is the anti-join idiom, and demoting it would return the exact opposite rows;∧— rejects if either side does;∨— only if both do;- anything computed from
c— a function call, an arithmetic expression — does not reject, conservatively. Relix hasNz,CoalesceandIIf, which exist precisely to map NULL to something truthy.
The ¬ arm is deliberately blunt: only ¬(c IS NULL) is treated as
rejecting. Under three-valued logic ¬(c = 5) also rejects — negating
unknown leaves it unknown, which is not true — but establishing that in
general needs the dual analysis ("can this be false when c is NULL?")
rather than an inversion of this one. Under-firing costs an optimization; getting it
wrong costs the answer.
Which side a column belongs to is resolved by JoinSides, so a reference
either input could own never counts as rejecting.
This class is package-private and stateless; call
apply(RelNode, String, SchemaAnnotations, OptimizationContext) as a static
method.
-
Method Summary