]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix nullability check for a sub-select's upper-level Vars
authorRichard Guo <rguo@postgresql.org>
Mon, 3 Aug 2026 06:51:59 +0000 (15:51 +0900)
committerRichard Guo <rguo@postgresql.org>
Mon, 3 Aug 2026 06:51:59 +0000 (15:51 +0900)
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 <zhaorui126@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Reviewed-by: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/CAHWVJhGuaFFRpmq4j+mcMcm_HC5QOT7LZsC9bf9b7BCBmvbfMA@mail.gmail.com
Backpatch-through: 19

src/backend/optimizer/util/clauses.c
src/test/regress/expected/subselect.out
src/test/regress/sql/subselect.sql

index 7d7f2f9664bb1b0c614f12e4b94050b78b7982c6..337fc27262eceae3c24edc654741ce5ab824a3aa 100644 (file)
@@ -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;
 
index e7ff719108245d010073c3af688f09b373b41943..ef49b5756a7d608e6feb95c1f37c5bb5676288cf 100644 (file)
@@ -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;
index 76bce5cdba5ca6e76f21419d174dc51dbbc7411d..0b18e0132aadbabecc73a2570fbd2e49f6f1f1c2 100644 (file)
@@ -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;