From: Richard Guo Date: Mon, 3 Aug 2026 06:51:59 +0000 (+0900) Subject: Fix nullability check for a sub-select's upper-level Vars X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=0d99579320d075eba5733517463d81a2231e373b;p=thirdparty%2Fpostgresql.git Fix nullability check for a sub-select's upper-level Vars When checking whether a sub-select's output columns can produce NULL, so as to decide whether a NOT IN can be converted to an anti-join, query_outputs_are_not_nullable() falls back on find_nonnullable_vars() for targetlist entries that are plain Vars: if the sub-select's own quals prove the Var non-null, the output is non-nullable. But that test compared only varno and varattno, without checking varlevelsup. An outer reference in the targetlist could thus be matched against a Var of the sub-select's own range table that happens to share the same varno and varattno, wrongly proving the output non-nullable and allowing an invalid conversion to an anti-join, which yields wrong answers when the outer reference is NULL. To fix, restrict the fallback to Vars of the current query level. Author: Rui Zhao Reviewed-by: Tender Wang Reviewed-by: Richard Guo Discussion: https://postgr.es/m/CAHWVJhGuaFFRpmq4j+mcMcm_HC5QOT7LZsC9bf9b7BCBmvbfMA@mail.gmail.com Backpatch-through: 19 --- diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7d7f2f9664b..337fc27262e 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2163,7 +2163,8 @@ query_outputs_are_not_nullable(Query *query) if (expr_is_nonnullable(&subroot, expr, NOTNULL_SOURCE_CATALOG)) continue; - if (IsA(expr, Var)) + /* Note we can only prove things about this query's own Vars */ + if (IsA(expr, Var) && ((Var *) expr)->varlevelsup == 0) { Var *var = (Var *) expr; diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index e7ff7191082..ef49b5756a7 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -3998,4 +3998,28 @@ WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); ----+----- (0 rows) +-- No ANTI JOIN: the sub-select's output is an upper-level Var, so the +-- sub-select's own quals tell us nothing about its nullability +INSERT INTO null_tab VALUES (1, NULL); +INSERT INTO not_null_tab VALUES (2, 2); +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab t1 +WHERE COALESCE(t1.id, -1) NOT IN + (SELECT t1.val FROM not_null_tab t2 WHERE t2.val IS NOT NULL); + QUERY PLAN +---------------------------------------------------------------------------- + Seq Scan on null_tab t1 + Filter: (NOT (ANY (COALESCE(id, '-1'::integer) = (SubPlan any_1).col1))) + SubPlan any_1 + -> Seq Scan on not_null_tab t2 +(4 rows) + +-- NOT IN with NULL on inner side should return no rows +SELECT * FROM null_tab t1 +WHERE COALESCE(t1.id, -1) NOT IN + (SELECT t1.val FROM not_null_tab t2 WHERE t2.val IS NOT NULL); + id | val +----+----- +(0 rows) + ROLLBACK; diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 76bce5cdba5..0b18e0132aa 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1751,4 +1751,19 @@ WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); SELECT * FROM not_null_tab WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); +-- No ANTI JOIN: the sub-select's output is an upper-level Var, so the +-- sub-select's own quals tell us nothing about its nullability +INSERT INTO null_tab VALUES (1, NULL); +INSERT INTO not_null_tab VALUES (2, 2); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab t1 +WHERE COALESCE(t1.id, -1) NOT IN + (SELECT t1.val FROM not_null_tab t2 WHERE t2.val IS NOT NULL); + +-- NOT IN with NULL on inner side should return no rows +SELECT * FROM null_tab t1 +WHERE COALESCE(t1.id, -1) NOT IN + (SELECT t1.val FROM not_null_tab t2 WHERE t2.val IS NOT NULL); + ROLLBACK;