You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Eliminate outer joins with empty relations via null-padded projection (apache#21321)
## Which issue does this PR close?
- Closesapache#21320
### Rationale for this change
When one side of a LEFT/RIGHT/FULL outer join is an EmptyRelation, the
current PropagateEmptyRelation optimizer rule leaves the join untouched.
This means the engine still builds a hash table for the empty side,
probes every row from the non-empty side, finds zero matches, and pads
NULLs — all wasted work.
The TODO at lines 76-80 of propagate_empty_relation.rs explicitly called
out this gap:
```
// TODO: For LeftOut/Full Join, if the right side is empty, the Join can be eliminated
// with a Projection with left side columns + right side columns replaced with null values.
// For RightOut/Full Join, if the left side is empty, the Join can be eliminated
// with a Projection with right side columns + left side columns replaced with null values.
```
### What changes are included in this PR?
Extends the PropagateEmptyRelation rule to handle 4 previously
unoptimized cases by replacing the join with a Projection that null-pads
the empty side's columns:
### Are these changes tested?
Yes. 4 new unit tests added:
### Are there any user-facing changes?
No API changes.
---------
Co-authored-by: Subham Singhal <subhamsinghal@Subhams-MacBook-Air.local>
Co-authored-by: Dmitrii Blaginin <dmitrii@blaginin.me>
let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
663
+
.filter(lit(false))?
664
+
.build()?;
665
+
666
+
let plan = LogicalPlanBuilder::from(left)
667
+
.join_using(
668
+
right_empty,
669
+
JoinType::Left,
670
+
vec![Column::from_name("a".to_string())],
671
+
)?
672
+
.build()?;
673
+
674
+
let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n TableScan: left";
let expected = "Projection: CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c, right.a, right.b, right.c\n TableScan: right";
let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
703
+
.filter(lit(false))?
704
+
.build()?;
705
+
706
+
let plan = LogicalPlanBuilder::from(left)
707
+
.join_using(
708
+
right_empty,
709
+
JoinType::Full,
710
+
vec![Column::from_name("a".to_string())],
711
+
)?
712
+
.build()?;
713
+
714
+
let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n TableScan: left";
let expected = "Projection: CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c, right.a, right.b, right.c\n TableScan: right";
let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
743
+
.filter(lit(false))?
744
+
.build()?;
745
+
746
+
// Complex ON condition: left.a = right.a AND left.b > right.b
747
+
let plan = LogicalPlanBuilder::from(left)
748
+
.join(
749
+
right_empty,
750
+
JoinType::Left,
751
+
(
752
+
vec![Column::from_name("a".to_string())],
753
+
vec![Column::from_name("a".to_string())],
754
+
),
755
+
Some(col("left.b").gt(col("right.b"))),
756
+
)?
757
+
.build()?;
758
+
759
+
let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n TableScan: left";
0 commit comments