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
x NOT IN (SELECT y FROM inner WHERE y = x) returns no rows when inner.y holds a NULL. The correct answer is every row of the outer table whose x is not in inner.y.
The correlation predicate y = x and the IN predicate x = y are the same equality. PullUpCorrelatedExpr removes the duplicate, so the correlation is gone from the plan. What is left is the uncorrelatedx NOT IN (SELECT y FROM inner), and that query is UNKNOWN for every row as soon as inner.y holds a NULL.
The removal is correct for IN (a LeftSemi join), because x IN (SELECT y FROM inner WHERE y = x) and x IN (SELECT y FROM inner) give the same answer. It is not correct for NOT IN. The correlation makes the subquery result {x} or the empty set, and neither can hold a NULL, so a correlated NOT IN of this shape is never UNKNOWN.
There is no error and no warning.
To Reproduce
CREATETABLEt1(id INT, z INT) ASVALUES (1,10), (2,20), (NULL,30), (4,40);
CREATETABLEt2(id INT, z INT) ASVALUES (1,5), (NULL,50), (5,10);
SELECT id FROM t1 WHERE id NOT IN (SELECTt2.idFROM t2 WHEREt2.id=t1.id) ORDER BY id;
datafusion-cli:
+----+
| id |
+----+
+----+
0 row(s) fetched.
Expected: 2, 4, NULL. DuckDB 1.5.2 gives those three rows.
Why 2, 4 and NULL are correct
The subquery result is different for each row of t1.
t1.id
subquery result
id NOT IN ...
keep the row?
1
{1}
FALSE
no
2
{}
TRUE
yes
NULL
{}
TRUE
yes
4
{}
TRUE
yes
The row t2.id = NULL never reaches a subquery result, because NULL = t1.id is UNKNOWN for every value of t1.id. Therefore no row of this query is UNKNOWN.
Full matrix
The same two tables. z = 40 is only there to move the NOT IN into a larger expression.
#
Query (... FROM t1 WHERE)
Correct
DataFusion
Q1
id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)
2, 4, NULL
(none) ❌
Q2
id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id) OR z = 40
2, 4, NULL
4 ❌
Q3
(id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)) IS NULL
(none)
2, 4, NULL ❌
Q4
SELECT id, id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id) AS m FROM t1
1 false, 2 true, 4 true, NULL true
same ✅
Q5
id IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)
1
1 ✅
C1
id NOT IN (SELECT t2.id FROM t2 WHERE t2.z = t1.z) (correlation on another column)
Q1, Q2 and Q3 are wrong. Q4 goes through the three-join materialization of a projected IN, which is not affected.
Expected behavior
Q1 and Q2 give 2, 4 and NULL. Q3 gives no rows.
Additional context
Tested with a release datafusion-cli built from main at c4f5a9e0f2. The results are the same on the branch of #25339, so that pull request does not change this shape.
EXPLAIN for Q1 shows that the correlation is gone. The subquery side is a bare TableScan: t2, and the join key is the IN equality:
let(mut join_filters, subquery_filters) = find_join_exprs(subquery_filter_exprs)?;ifletSome(in_predicate) = &self.in_predicate_opt{// in_predicate may be already included in the join filters, remove it from the join filters first.
join_filters = remove_duplicated_filter(join_filters, in_predicate)?;}
remove_duplicated_filter also treats the swapped operand order as the same expression, so t2.id = t1.id matches the IN predicate t1.id = t2.id.
For a LeftSemi join the removal changes nothing. For a null-aware LeftAnti or LeftMark join it drops the very predicate that keeps the NULLs out of the subquery result.
Or, because the subquery result of this shape can never hold a NULL, plan it as a plain (not null-aware) anti join plus the x IS NULL case. This is the cheaper plan, but it needs the optimizer to prove that the correlation is an equality on the IN value expression itself.
Checked against the open work on the same code
#25273 is the only open pull request that changes remove_duplicated_filter. It does not change this shape. A datafusion-cli built from its head ff6c625b09 gives the same three wrong results and the same plan as main:
That pull request moves where the outer reference markers are removed: find_join_exprs keeps them, and remove_duplicated_filter removes them itself before it compares. The pair still matches, so the correlation is still dropped.
Describe the bug
x NOT IN (SELECT y FROM inner WHERE y = x)returns no rows wheninner.yholds a NULL. The correct answer is every row of the outer table whosexis not ininner.y.The correlation predicate
y = xand theINpredicatex = yare the same equality.PullUpCorrelatedExprremoves the duplicate, so the correlation is gone from the plan. What is left is the uncorrelatedx NOT IN (SELECT y FROM inner), and that query is UNKNOWN for every row as soon asinner.yholds a NULL.The removal is correct for
IN(aLeftSemijoin), becausex IN (SELECT y FROM inner WHERE y = x)andx IN (SELECT y FROM inner)give the same answer. It is not correct forNOT IN. The correlation makes the subquery result{x}or the empty set, and neither can hold a NULL, so a correlatedNOT INof this shape is never UNKNOWN.There is no error and no warning.
To Reproduce
datafusion-cli:Expected:
2,4,NULL. DuckDB 1.5.2 gives those three rows.Why
2,4andNULLare correctThe subquery result is different for each row of
t1.t1.idid NOT IN ...{1}{}{}{}The row
t2.id = NULLnever reaches a subquery result, becauseNULL = t1.idis UNKNOWN for every value oft1.id. Therefore no row of this query is UNKNOWN.Full matrix
The same two tables.
z = 40is only there to move theNOT INinto a larger expression.... FROM t1 WHERE)id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)2, 4, NULLid NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id) OR z = 402, 4, NULL4❌(id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)) IS NULL2, 4, NULL❌SELECT id, id NOT IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id) AS m FROM t11 false, 2 true, 4 true, NULL trueid IN (SELECT t2.id FROM t2 WHERE t2.id = t1.id)11✅id NOT IN (SELECT t2.id FROM t2 WHERE t2.z = t1.z)(correlation on another column)1, 2, 4, NULLid NOT IN (SELECT t2.id FROM t2)(no correlation)Q1, Q2 and Q3 are wrong. Q4 goes through the three-join materialization of a projected
IN, which is not affected.Expected behavior
Q1 and Q2 give
2,4andNULL. Q3 gives no rows.Additional context
Tested with a release
datafusion-clibuilt frommainatc4f5a9e0f2. The results are the same on the branch of #25339, so that pull request does not change this shape.EXPLAINfor Q1 shows that the correlation is gone. The subquery side is a bareTableScan: t2, and the join key is theINequality:This is the plan of the uncorrelated
SELECT id FROM t1 WHERE id NOT IN (SELECT id FROM t2), which correctly gives no rows.Suspected root cause
PullUpCorrelatedExprremoves a correlation filter that is the same expression as theINpredicate:https://github.com/apache/datafusion/blob/c4f5a9e0f2/datafusion/optimizer/src/decorrelate.rs#L185-L190
remove_duplicated_filteralso treats the swapped operand order as the same expression, sot2.id = t1.idmatches theINpredicatet1.id = t2.id.For a
LeftSemijoin the removal changes nothing. For a null-awareLeftAntiorLeftMarkjoin it drops the very predicate that keeps the NULLs out of the subquery result.Fix sketch
LeftAntiorLeftMarkjoin. The join then has theINvalue key and the correlation scope key, which fix: correlated NOT IN with a non-equality correlation returns wrong results #25339 supports. Note thaton[0]must stay theINvalue key.x IS NULLcase. This is the cheaper plan, but it needs the optimizer to prove that the correlation is an equality on theINvalue expression itself.Checked against the open work on the same code
#25273 is the only open pull request that changes
remove_duplicated_filter. It does not change this shape. Adatafusion-clibuilt from its headff6c625b09gives the same three wrong results and the same plan asmain:That pull request moves where the outer reference markers are removed:
find_join_exprskeeps them, andremove_duplicated_filterremoves them itself before it compares. The pair still matches, so the correlation is still dropped.Related
NOT INwith a non-equality correlation. Found while the review of fix: correlated NOT IN with a non-equality correlation returns wrong results #25339 checked the shapes around it.NOT IN (subquery)ignores NULLs in the subquery #25347 — a correlatedNOT INwith a constant value.remove_duplicated_filterfor a different bug (When performing set comparison subqueries (such as > ANY / > ALL / = ANY, etc.) on the same table (without using aliases), silently returns wrong results (0 rows or all rows). #25258). The two changes touch the same function.