From dfd2ee200414150ecf805a08827bfcdd9893e36d Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Thu, 30 Jul 2026 23:02:56 +0800 Subject: [PATCH 1/2] Don't take a local Var's non-nullness as proof about an upper-level Var find_nonnullable_vars() only reports Vars of the current query level, and the multibitmapset it returns identifies them by varno and varattno alone. A NOT IN whose left-hand side is an upper-level Var can therefore collide with a local Var that happens to have the same varno and varattno, and be proven non-nullable by a qual that says nothing about it. The NOT IN is then converted to an anti-join and rows the original would have discarded come back. Restrict the qual-based proof to level-zero Vars, and add a test. Reported-by: Ayush Tiwari --- src/backend/optimizer/plan/subselect.c | 8 ++++- src/test/regress/expected/subselect.out | 40 +++++++++++++++++++++++++ src/test/regress/sql/subselect.sql | 23 ++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index f6afcff740..94b6a2d8f6 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -1601,11 +1601,17 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink, * on which this SubLink is evaluated. Compute the list of Vars they * force non-null if we didn't already. * + * find_nonnullable_vars only reports Vars of the current query level, + * and identifies them by varno and varattno alone, so an upper-level + * Var could be mistaken for a local one with the same varno/varattno. + * Hence the varlevelsup test. + * * Note that the quals must be run through flatten_join_alias_vars, * just as the outer expressions were above, so that the Vars match * up. */ - if (nonnullable_quals != NIL && IsA(expr, Var)) + if (nonnullable_quals != NIL && 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 68fcb02ac7..bfba1d532f 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -3759,6 +3759,46 @@ ORDER BY a; 3 | 7 (1 row) +-- No ANTI JOIN: one left-hand expression is an upper-level Var. A qual on a +-- local Var with the same varno and varattno must not be taken as proof about +-- it. +CREATE TEMP TABLE upper_outer (a int); +CREATE TEMP TABLE upper_local (a int); +CREATE TEMP TABLE upper_inner (x int NOT NULL, y int NOT NULL); +INSERT INTO upper_outer VALUES (NULL), (10); +INSERT INTO upper_local VALUES (1); +INSERT INTO upper_inner VALUES (1, 999); +EXPLAIN (COSTS OFF) +SELECT o.a, + ARRAY(SELECT l.a FROM upper_local l + WHERE l.a IS NOT NULL + AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i)) +FROM upper_outer o +ORDER BY o.a NULLS FIRST; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------- + Sort + Sort Key: o.a NULLS FIRST + -> Seq Scan on upper_outer o + SubPlan array_1 + -> Seq Scan on upper_local l + Filter: ((a IS NOT NULL) AND (NOT (ANY ((a = (hashed SubPlan any_1).col1) AND (o.a = (hashed SubPlan any_1).col2))))) + SubPlan any_1 + -> Seq Scan on upper_inner i +(8 rows) + +SELECT o.a, + ARRAY(SELECT l.a FROM upper_local l + WHERE l.a IS NOT NULL + AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i)) +FROM upper_outer o +ORDER BY o.a NULLS FIRST; + a | array +----+------- + | {} + 10 | {1} +(2 rows) + -- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL -- and is not nulled by outer join EXPLAIN (COSTS OFF) diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 6486ed92da..4d00786ee3 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1643,6 +1643,29 @@ SELECT * FROM qual_outer WHERE b IS NOT NULL AND b NOT IN (SELECT y FROM qual_inner) ORDER BY a; +-- No ANTI JOIN: one left-hand expression is an upper-level Var. A qual on a +-- local Var with the same varno and varattno must not be taken as proof about +-- it. +CREATE TEMP TABLE upper_outer (a int); +CREATE TEMP TABLE upper_local (a int); +CREATE TEMP TABLE upper_inner (x int NOT NULL, y int NOT NULL); +INSERT INTO upper_outer VALUES (NULL), (10); +INSERT INTO upper_local VALUES (1); +INSERT INTO upper_inner VALUES (1, 999); +EXPLAIN (COSTS OFF) +SELECT o.a, + ARRAY(SELECT l.a FROM upper_local l + WHERE l.a IS NOT NULL + AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i)) +FROM upper_outer o +ORDER BY o.a NULLS FIRST; +SELECT o.a, + ARRAY(SELECT l.a FROM upper_local l + WHERE l.a IS NOT NULL + AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i)) +FROM upper_outer o +ORDER BY o.a NULLS FIRST; + -- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL -- and is not nulled by outer join EXPLAIN (COSTS OFF) -- 2.43.7