Skip to content

Commit 5148cd3

Browse files
adriangbclaude
andcommitted
refactor: use the null-aware anti join for every NOT IN key shape
#25339 made the null-aware `LeftAnti` hash join take more than one key and a residual filter. The two workarounds for the old executor limits are no longer necessary: * More than one key: the rule built a null-aware `LeftMark` join and a filter on the mark. It now builds the null-aware `LeftAnti` join. * A residual filter: the rule gave up, and the caller materialized the UNKNOWN rows with three mark joins, two of them nested loop joins. It now builds the null-aware `LeftAnti` join, which applies the residual filter when it looks for UNKNOWN rows. The plan in `joins.slt` is the same as on `main` again, and the single anti join that `null_aware_anti_join.slt` pins for a residual filter stays. The executor reads the first key as the `NOT IN` value. If the `IN` equality does not become that key, the rule still gives up and the caller materializes the result. A constant `NOT IN` value is projected as a column for a correlated subquery too, so the value is the first key and the correlation the second. This corrects two `null_aware_anti_join.slt` results that were pinned to wrong answers, and three constant `NOT IN` queries that failed to plan now run. All the new results agree with DuckDB 1.5.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 10ccc3e commit 5148cd3

4 files changed

Lines changed: 89 additions & 141 deletions

File tree

‎datafusion/optimizer/src/decorrelate_predicate_subquery.rs‎

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -488,12 +488,12 @@ struct MarkJoin {
488488

489489
/// The join keys of the join that replaces an `IN` or `NOT IN` predicate.
490490
struct JoinKeys {
491-
/// The equalities that the hash join can use as keys. The `IN` predicate
492-
/// is the first one when its value holds a column; the others are the
493-
/// correlation.
494-
equijoin_keys: Vec<(Expr, Expr)>,
495491
/// The part of the join filter that the split could not turn into keys.
496492
residual_filter: Option<Expr>,
493+
/// True if the `IN` equality is the first equality that the hash join can
494+
/// use as a key, which is where a null-aware hash join reads the `IN`
495+
/// value. The other keys are the correlation.
496+
value_is_first_key: bool,
497497
/// True if the `IN` value or the subquery column it is compared with can
498498
/// be NULL inside the scope of an outer row, see
499499
/// [`key_may_be_null_in_scope`]. Only then can `IN` be UNKNOWN, so only
@@ -519,9 +519,12 @@ impl JoinKeys {
519519
left_schema,
520520
right_schema,
521521
)?;
522+
let value_is_first_key = equijoin_keys.first().is_some_and(|(value, column)| {
523+
value == &in_value.value && column == &in_value.subquery_column
524+
});
522525
Ok(Self {
523-
equijoin_keys,
524526
residual_filter,
527+
value_is_first_key,
525528
value_may_be_null: in_value.may_be_null_in_scope(
526529
left_schema,
527530
right_schema,
@@ -764,8 +767,6 @@ fn build_join(
764767
}
765768
other => other,
766769
};
767-
// The columns of the outer plan, which an anti join keeps as they are.
768-
let outer_columns = left.schema().columns();
769770
let left = projected_left.as_ref().unwrap_or(left);
770771

771772
let join_filter = match (&in_value, join_filter_opt) {
@@ -799,32 +800,19 @@ fn build_join(
799800
};
800801

801802
// A `NOT IN` in a filter builds a `LeftAnti` join, and needs null-aware
802-
// semantics when the value can be NULL in scope. The null-aware `LeftAnti`
803-
// executor takes one key only (see `NullAwareMode::try_new`), and no hash
804-
// join can mark the UNKNOWN rows of a residual filter
805-
// (https://github.com/apache/datafusion/issues/25336). So:
806-
//
807-
// * A residual filter: give up here. The caller then materializes the
808-
// UNKNOWN rows with more joins, see `in_subquery_value_mark_join`.
809-
// * More than one key: the null-aware `LeftMark` executor takes any number
810-
// of keys, the others being the scope of the outer row. Build that join
811-
// instead and keep the rows whose mark is FALSE, which is `NOT IN` under
812-
// three-valued logic.
813-
// * One key: the null-aware `LeftAnti` join below.
814-
let anti_join_as_mark = match &join_keys {
815-
Some(keys) if join_type == JoinType::LeftAnti && keys.value_may_be_null => {
816-
if keys.residual_filter.is_some() {
817-
return Ok(None);
818-
}
819-
keys.equijoin_keys.len() > 1
820-
}
821-
_ => false,
822-
};
823-
let join_type = if anti_join_as_mark {
824-
JoinType::LeftMark
825-
} else {
826-
join_type
827-
};
803+
// semantics when the value can be NULL in scope. The null-aware hash join
804+
// reads its first key as the `NOT IN` value and the other keys as the
805+
// scope of the outer row (see `HashJoinExec::null_aware`). If the `IN`
806+
// equality did not become that first key, give up here. The caller then
807+
// materializes the UNKNOWN rows with more joins, see
808+
// `in_subquery_value_mark_join`.
809+
if let Some(keys) = &join_keys
810+
&& join_type == JoinType::LeftAnti
811+
&& keys.value_may_be_null
812+
&& !keys.value_is_first_key
813+
{
814+
return Ok(None);
815+
}
828816

829817
if matches!(join_type, JoinType::LeftMark | JoinType::RightMark) {
830818
let right_schema = sub_query_alias.schema();
@@ -884,18 +872,6 @@ fn build_join(
884872
)?
885873
.build()?;
886874

887-
// `NOT IN` keeps the rows whose mark is FALSE. `NOT mark` is TRUE for
888-
// those rows only, and the projection removes the mark column again.
889-
let new_plan = if anti_join_as_mark {
890-
let mark = Expr::Column(Column::new(Some(alias.to_string()), "mark"));
891-
LogicalPlanBuilder::from(new_plan)
892-
.filter(not(mark))?
893-
.project(outer_columns.into_iter().map(Expr::from))?
894-
.build()?
895-
} else {
896-
new_plan
897-
};
898-
899875
debug!(
900876
"predicate subquery optimized:\n{}",
901877
new_plan.display_indent()
@@ -909,8 +885,7 @@ fn build_join(
909885

910886
// Null-aware semantics are only needed for a `NOT IN` anti join, which
911887
// follows three-valued logic. `NOT EXISTS` and `IN` are two-valued, and
912-
// `join_keys` is `None` for them. The join here has one key and no
913-
// residual filter: the other shapes were handled above.
888+
// `join_keys` is `None` for them.
914889
let null_aware = join_keys
915890
.as_ref()
916891
.is_some_and(|keys| keys.value_may_be_null);
@@ -1895,11 +1870,11 @@ mod tests {
18951870
)
18961871
}
18971872

1898-
/// The same rewrite must not fire for a correlated subquery: the
1899-
/// correlation predicate is a second equi-join key, and null-aware hash
1900-
/// joins accept only one.
1873+
/// The same rewrite fires for a correlated subquery. The projected value
1874+
/// is the first equi-join key, which is where the null-aware hash join
1875+
/// reads the `NOT IN` value, and the correlation is the second.
19011876
#[test]
1902-
fn constant_not_in_correlated_subquery_becomes_a_mark_join() -> Result<()> {
1877+
fn constant_not_in_correlated_subquery() -> Result<()> {
19031878
let outer_scan = nullable_scalar_mark_scan("outer_t")?;
19041879
let inner_scan = nullable_scalar_mark_scan("inner_t")?;
19051880

@@ -1920,14 +1895,12 @@ mod tests {
19201895
plan,
19211896
@"
19221897
Projection: outer_t.id, outer_t.grp [id:Int32;N, grp:Int32;N]
1923-
Filter: NOT __correlated_sq_1.mark [id:Int32;N, grp:Int32;N, __correlated_sq_1_value:Int32, mark:Boolean;N]
1924-
LeftMark Join: Filter: __correlated_sq_1_value = __correlated_sq_1.id AND outer_t.grp = __correlated_sq_1.grp null_aware [id:Int32;N, grp:Int32;N, __correlated_sq_1_value:Int32, mark:Boolean;N]
1925-
Projection: outer_t.id, outer_t.grp, Int32(3) AS __correlated_sq_1_value [id:Int32;N, grp:Int32;N, __correlated_sq_1_value:Int32]
1926-
TableScan: outer_t [id:Int32;N, grp:Int32;N]
1927-
Projection: __correlated_sq_1.id, __correlated_sq_1.grp [id:Int32;N, grp:Int32;N]
1928-
SubqueryAlias: __correlated_sq_1 [id:Int32;N, grp:Int32;N]
1929-
Projection: inner_t.id, inner_t.grp [id:Int32;N, grp:Int32;N]
1930-
TableScan: inner_t [id:Int32;N, grp:Int32;N]
1898+
LeftAnti Join: Filter: __correlated_sq_1_value = __correlated_sq_1.id AND outer_t.grp = __correlated_sq_1.grp null_aware [id:Int32;N, grp:Int32;N, __correlated_sq_1_value:Int32]
1899+
Projection: outer_t.id, outer_t.grp, Int32(3) AS __correlated_sq_1_value [id:Int32;N, grp:Int32;N, __correlated_sq_1_value:Int32]
1900+
TableScan: outer_t [id:Int32;N, grp:Int32;N]
1901+
SubqueryAlias: __correlated_sq_1 [id:Int32;N, grp:Int32;N]
1902+
Projection: inner_t.id, inner_t.grp [id:Int32;N, grp:Int32;N]
1903+
TableScan: inner_t [id:Int32;N, grp:Int32;N]
19311904
"
19321905
)
19331906
}

‎datafusion/sqllogictest/test_files/joins.slt‎

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,22 +1988,11 @@ where join_t1.t1_id + 12 not in
19881988
(select join_t2.t2_id + 1 from join_t2 where join_t1.t1_int > 0)
19891989
----
19901990
logical_plan
1991-
01)Projection: join_t1.t1_id, join_t1.t1_name, join_t1.t1_int
1992-
02)--Filter: NOT CASE WHEN __correlated_sq_2.mark THEN Boolean(true) WHEN __correlated_sq_3.mark OR CAST(join_t1.t1_id AS Int64) + Int64(12) IS NULL AND __correlated_sq_4.mark THEN Boolean(NULL) ELSE Boolean(false) END
1993-
03)----LeftMark Join: Filter: join_t1.t1_int > UInt32(0)
1994-
04)------LeftMark Join: Filter: join_t1.t1_int > UInt32(0)
1995-
05)--------LeftMark Join: CAST(join_t1.t1_id AS Int64) + Int64(12) = __correlated_sq_2.join_t2.t2_id + Int64(1) Filter: join_t1.t1_int > UInt32(0)
1996-
06)----------TableScan: join_t1 projection=[t1_id, t1_name, t1_int]
1997-
07)----------SubqueryAlias: __correlated_sq_2
1998-
08)------------Projection: CAST(join_t2.t2_id AS Int64) + Int64(1)
1999-
09)--------------TableScan: join_t2 projection=[t2_id]
2000-
10)--------SubqueryAlias: __correlated_sq_3
2001-
11)----------Projection: CAST(join_t2.t2_id AS Int64) + Int64(1)
2002-
12)------------Filter: CAST(join_t2.t2_id AS Int64) + Int64(1) IS NULL
2003-
13)--------------TableScan: join_t2 projection=[t2_id]
2004-
14)------SubqueryAlias: __correlated_sq_4
2005-
15)--------Projection: CAST(join_t2.t2_id AS Int64) + Int64(1)
2006-
16)----------TableScan: join_t2 projection=[t2_id]
1991+
01)LeftAnti Join: CAST(join_t1.t1_id AS Int64) + Int64(12) = __correlated_sq_1.join_t2.t2_id + Int64(1) Filter: join_t1.t1_int > UInt32(0) null_aware
1992+
02)--TableScan: join_t1 projection=[t1_id, t1_name, t1_int]
1993+
03)--SubqueryAlias: __correlated_sq_1
1994+
04)----Projection: CAST(join_t2.t2_id AS Int64) + Int64(1)
1995+
05)------TableScan: join_t2 projection=[t2_id]
20071996

20081997
# In subquery to join with outer filter
20091998

‎datafusion/sqllogictest/test_files/null_aware_anti_join.slt‎

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -655,11 +655,15 @@ CREATE TABLE naconst_corr_t2(id INT, g INT) AS VALUES (1, 1), (NULL, 2);
655655
# A non-equality correlation stays in the join filter. Row (1, 1) sees the NULL
656656
# of (NULL, 2), so its `NOT IN` is UNKNOWN; row (2, 2) sees an empty subquery,
657657
# so its `NOT IN` is TRUE. Expected results verified with DuckDB.
658-
query error DataFusion error: Error during planning: null_aware LeftAnti join requires equi\-join keys, but the join has none
658+
query I
659659
SELECT id FROM naconst_corr_t1 WHERE 3 NOT IN (SELECT id FROM naconst_corr_t2 WHERE naconst_corr_t2.g > naconst_corr_t1.g) ORDER BY id;
660+
----
661+
2
660662

661-
query error DataFusion error: Error during planning: null_aware LeftAnti join requires equi\-join keys, but the join has none
663+
query I
662664
SELECT id FROM naconst_corr_t1 WHERE NOT (3 IN (SELECT id FROM naconst_corr_t2 WHERE naconst_corr_t2.g > naconst_corr_t1.g)) ORDER BY id;
665+
----
666+
2
663667

664668
statement ok
665669
DROP TABLE naconst_corr_t1;
@@ -683,17 +687,17 @@ query I
683687
SELECT z FROM naconst_corr_t3 WHERE 1 NOT IN (SELECT naconst_corr_t4.id FROM naconst_corr_t4 WHERE naconst_corr_t4.z = naconst_corr_t3.z) ORDER BY z;
684688
----
685689
7
686-
10
690+
NULL
687691

688-
# Pinned to today's behaviour, which is incorrect. See
689-
# https://github.com/apache/datafusion/issues/25336 -- the fix flips this.
690692
query I
691693
SELECT z FROM naconst_corr_t3 WHERE 2 NOT IN (SELECT naconst_corr_t4.id FROM naconst_corr_t4 WHERE naconst_corr_t4.z = naconst_corr_t3.z) ORDER BY z;
692694
----
693-
10
695+
NULL
694696

695-
query error DataFusion error: Error during planning: null_aware LeftAnti join requires equi\-join keys, but the join has none
697+
query I
696698
SELECT z FROM naconst_corr_t3 WHERE 1 NOT IN (SELECT naconst_corr_t4.id FROM naconst_corr_t4 WHERE naconst_corr_t4.z > naconst_corr_t3.z) ORDER BY z;
699+
----
700+
NULL
697701

698702
# The projected value column must be the FIRST equi-join key: a null-aware hash
699703
# join reads `on[0]` as the `NOT IN` value key and `on[1..]` as correlation
@@ -702,16 +706,19 @@ query TT
702706
EXPLAIN SELECT z FROM naconst_corr_t3 WHERE 1 NOT IN (SELECT naconst_corr_t4.id FROM naconst_corr_t4 WHERE naconst_corr_t4.z = naconst_corr_t3.z);
703707
----
704708
logical_plan
705-
01)LeftAnti Join: naconst_corr_t3.z = __correlated_sq_1.z Filter: Int64(1) = __correlated_sq_1.naconst_corr_t4.id null_aware
706-
02)--TableScan: naconst_corr_t3 projection=[z]
707-
03)--SubqueryAlias: __correlated_sq_1
708-
04)----Projection: CAST(naconst_corr_t4.id AS Int64), naconst_corr_t4.z
709-
05)------TableScan: naconst_corr_t4 projection=[id, z]
709+
01)Projection: naconst_corr_t3.z
710+
02)--LeftAnti Join: __correlated_sq_1_value = __correlated_sq_1.naconst_corr_t4.id, naconst_corr_t3.z = __correlated_sq_1.z null_aware
711+
03)----Projection: naconst_corr_t3.z, Int64(1) AS __correlated_sq_1_value
712+
04)------TableScan: naconst_corr_t3 projection=[z]
713+
05)----SubqueryAlias: __correlated_sq_1
714+
06)------Projection: CAST(naconst_corr_t4.id AS Int64), naconst_corr_t4.z
715+
07)--------TableScan: naconst_corr_t4 projection=[id, z]
710716
physical_plan
711-
01)HashJoinExec: mode=CollectLeft, join_type=LeftAnti, on=[(z@0, z@1)], filter=1 = naconst_corr_t4.id@0, null_aware
712-
02)--DataSourceExec: partitions=1, partition_sizes=[1]
713-
03)--ProjectionExec: expr=[CAST(id@0 AS Int64) as naconst_corr_t4.id, z@1 as z]
714-
04)----DataSourceExec: partitions=1, partition_sizes=[1]
717+
01)HashJoinExec: mode=CollectLeft, join_type=LeftAnti, on=[(__correlated_sq_1_value@1, naconst_corr_t4.id@0), (z@0, z@1)], projection=[z@0], null_aware
718+
02)--ProjectionExec: expr=[z@0 as z, 1 as __correlated_sq_1_value]
719+
03)----DataSourceExec: partitions=1, partition_sizes=[1]
720+
04)--ProjectionExec: expr=[CAST(id@0 AS Int64) as naconst_corr_t4.id, z@1 as z]
721+
05)----DataSourceExec: partitions=1, partition_sizes=[1]
715722

716723
statement ok
717724
DROP TABLE naconst_corr_t3;

‎datafusion/sqllogictest/test_files/subquery_projection.slt‎

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,8 @@ statement ok
369369
DROP TABLE r_empty;
370370

371371
# A correlated `NOT IN` filter builds a `LeftAnti` join with two keys: the
372-
# value and the correlation. A null-aware `LeftAnti` hash join supports one key
373-
# only, so a function key over non-nullable columns must not make this join
374-
# null-aware.
372+
# value and the correlation. A function key over non-nullable columns that
373+
# cannot be NULL must not make this join null-aware.
375374
statement ok
376375
CREATE TABLE t1(k INT NOT NULL, s VARCHAR NOT NULL) AS VALUES (1, 'a'), (2, 'b');
377376

@@ -385,9 +384,9 @@ SELECT * FROM t1 WHERE upper(t1.s) NOT IN (SELECT t2.s FROM t2 WHERE t2.k = t1.k
385384

386385
# `NULLIF(k, 1)` is NULL for `k = 1`, and that group of `t2` is not empty, so
387386
# the correct result has no row for `k = 1`. The join has two keys, the value
388-
# and the correlation, and the null-aware `LeftAnti` executor takes one key
389-
# only. The `NOT IN` becomes a null-aware mark join, which takes any number of
390-
# keys, and a filter on the mark. `main` keeps the `k = 1` row, which is
387+
# first and the correlation second, and is null-aware because the value
388+
# expression can be NULL. `main` tests the columns of the key instead, which
389+
# are not nullable, and keeps the `k = 1` row. That is
391390
# https://github.com/apache/datafusion/issues/25347.
392391
query I rowsort
393392
SELECT k FROM t1 WHERE NULLIF(t1.k, 1) NOT IN (SELECT t2.k + 10 FROM t2 WHERE t2.k = t1.k);
@@ -398,36 +397,29 @@ query TT
398397
EXPLAIN SELECT k FROM t1 WHERE NULLIF(t1.k, 1) NOT IN (SELECT t2.k + 10 FROM t2 WHERE t2.k = t1.k);
399398
----
400399
logical_plan
401-
01)Projection: t1.k
402-
02)--Filter: NOT __correlated_sq_1.mark
403-
03)----LeftMark Join: nullif(CAST(t1.k AS Int64), Int64(1)) = __correlated_sq_1.t2.k + Int64(10), t1.k = __correlated_sq_1.k null_aware
404-
04)------TableScan: t1 projection=[k]
405-
05)------SubqueryAlias: __correlated_sq_1
406-
06)--------Projection: CAST(t2.k AS Int64) + Int64(10), t2.k
407-
07)----------TableScan: t2 projection=[k]
400+
01)LeftAnti Join: nullif(CAST(t1.k AS Int64), Int64(1)) = __correlated_sq_1.t2.k + Int64(10), t1.k = __correlated_sq_1.k null_aware
401+
02)--TableScan: t1 projection=[k]
402+
03)--SubqueryAlias: __correlated_sq_1
403+
04)----Projection: CAST(t2.k AS Int64) + Int64(10), t2.k
404+
05)------TableScan: t2 projection=[k]
408405
physical_plan
409-
01)FilterExec: NOT mark@1, projection=[k@0]
410-
02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1
411-
03)----HashJoinExec: mode=CollectLeft, join_type=LeftMark, on=[(nullif(t1.k,Int64(1))@1, t2.k + Int64(10)@0), (k@0, k@1)], projection=[k@0, mark@2], null_aware
412-
04)------ProjectionExec: expr=[k@0 as k, nullif(CAST(k@0 AS Int64), 1) as nullif(t1.k,Int64(1))]
413-
05)--------DataSourceExec: partitions=1, partition_sizes=[1]
414-
06)------ProjectionExec: expr=[CAST(k@0 AS Int64) + 10 as t2.k + Int64(10), k@0 as k]
415-
07)--------DataSourceExec: partitions=1, partition_sizes=[1]
406+
01)HashJoinExec: mode=CollectLeft, join_type=LeftAnti, on=[(nullif(t1.k,Int64(1))@1, t2.k + Int64(10)@0), (k@0, k@1)], projection=[k@0], null_aware
407+
02)--ProjectionExec: expr=[k@0 as k, nullif(CAST(k@0 AS Int64), 1) as nullif(t1.k,Int64(1))]
408+
03)----DataSourceExec: partitions=1, partition_sizes=[1]
409+
04)--ProjectionExec: expr=[CAST(k@0 AS Int64) + 10 as t2.k + Int64(10), k@0 as k]
410+
05)----DataSourceExec: partitions=1, partition_sizes=[1]
416411

417412
statement ok
418413
DROP TABLE t1;
419414

420415
statement ok
421416
DROP TABLE t2;
422417

423-
# A non-equality correlation leaves one key and a residual join filter. No hash
424-
# join can mark the UNKNOWN rows of a residual filter
425-
# (https://github.com/apache/datafusion/issues/25336), so a `NOT IN` whose
426-
# value can be NULL does not become an anti join. It becomes the mark joins
427-
# that materialize its three-valued result, the same plan as for a projected
428-
# `IN`, and a filter on that result. For `k = 1` the key is NULL and the
429-
# correlated subquery result is empty, and `NULL NOT IN (<empty set>)` is TRUE.
430-
# Both rows are correct.
418+
# A non-equality correlation leaves one key and a residual join filter. The
419+
# null-aware `LeftAnti` hash join applies the residual filter when it looks
420+
# for UNKNOWN rows. For `k = 1` the key is NULL and the correlated subquery
421+
# result is empty, and `NULL NOT IN (<empty set>)` is TRUE. Both rows are
422+
# correct.
431423
statement ok
432424
CREATE TABLE ra(k INT NOT NULL, z INT NOT NULL) AS VALUES (1, 10), (2, 20);
433425

@@ -453,30 +445,17 @@ EXPLAIN SELECT k FROM ra WHERE NULLIF(ra.k, 1) NOT IN (SELECT rb.k FROM rb WHERE
453445
----
454446
logical_plan
455447
01)Projection: ra.k
456-
02)--Filter: NOT CASE WHEN __correlated_sq_2.mark THEN Boolean(true) WHEN __correlated_sq_3.mark OR nullif(CAST(ra.k AS Int64), Int64(1)) IS NULL AND __correlated_sq_4.mark THEN Boolean(NULL) ELSE Boolean(false) END
457-
03)----Projection: ra.k, __correlated_sq_2.mark, __correlated_sq_3.mark, __correlated_sq_4.mark
458-
04)------LeftMark Join: Filter: __correlated_sq_4.z > ra.z
459-
05)--------LeftMark Join: Filter: __correlated_sq_3.z > ra.z
460-
06)----------LeftMark Join: nullif(CAST(ra.k AS Int64), Int64(1)) = __correlated_sq_2.rb.k Filter: __correlated_sq_2.z > ra.z
461-
07)------------TableScan: ra projection=[k, z]
462-
08)------------SubqueryAlias: __correlated_sq_2
463-
09)--------------Projection: CAST(rb.k AS Int64), rb.z
464-
10)----------------TableScan: rb projection=[k, z]
465-
11)----------EmptyRelation: rows=0
466-
12)--------SubqueryAlias: __correlated_sq_4
467-
13)----------TableScan: rb projection=[z]
448+
02)--LeftAnti Join: nullif(CAST(ra.k AS Int64), Int64(1)) = __correlated_sq_1.rb.k Filter: __correlated_sq_1.z > ra.z null_aware
449+
03)----TableScan: ra projection=[k, z]
450+
04)----SubqueryAlias: __correlated_sq_1
451+
05)------Projection: CAST(rb.k AS Int64), rb.z
452+
06)--------TableScan: rb projection=[k, z]
468453
physical_plan
469-
01)FilterExec: NOT CASE WHEN mark@1 THEN true WHEN mark@2 OR nullif(CAST(k@0 AS Int64), 1) IS NULL AND mark@3 THEN NULL ELSE false END, projection=[k@0]
470-
02)--NestedLoopJoinExec: join_type=RightMark, filter=z@1 > z@0, projection=[k@0, mark@2, mark@3, mark@4]
454+
01)HashJoinExec: mode=CollectLeft, join_type=LeftAnti, on=[(nullif(ra.k,Int64(1))@2, rb.k@0)], filter=z@1 > z@0, projection=[k@0], null_aware
455+
02)--ProjectionExec: expr=[k@0 as k, z@1 as z, nullif(CAST(k@0 AS Int64), 1) as nullif(ra.k,Int64(1))]
471456
03)----DataSourceExec: partitions=1, partition_sizes=[1]
472-
04)----NestedLoopJoinExec: join_type=RightMark, filter=z@1 > z@0
473-
05)------EmptyExec
474-
06)------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1
475-
07)--------HashJoinExec: mode=CollectLeft, join_type=RightMark, on=[(rb.k@0, nullif(ra.k,Int64(1))@2)], filter=z@1 > z@0, projection=[k@0, z@1, mark@3]
476-
08)----------ProjectionExec: expr=[CAST(k@0 AS Int64) as rb.k, z@1 as z]
477-
09)------------DataSourceExec: partitions=1, partition_sizes=[1]
478-
10)----------ProjectionExec: expr=[k@0 as k, z@1 as z, nullif(CAST(k@0 AS Int64), 1) as nullif(ra.k,Int64(1))]
479-
11)------------DataSourceExec: partitions=1, partition_sizes=[1]
457+
04)--ProjectionExec: expr=[CAST(k@0 AS Int64) as rb.k, z@1 as z]
458+
05)----DataSourceExec: partitions=1, partition_sizes=[1]
480459

481460
statement ok
482461
DROP TABLE ra;

0 commit comments

Comments
 (0)