Re: Prove a NOT IN's left-hand expressions non-nullable from quals

From: Ayush Tiwari <ayushtiwari(dot)slg01(at)gmail(dot)com>
To: Richard Guo <guofenglinux(at)gmail(dot)com>
Cc: Pg Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: Re: Prove a NOT IN's left-hand expressions non-nullable from quals
Date: 2026-07-29 05:41:24
Message-ID: CAJTYsWWH6Pm2xiq=OM3vnBEBz5=Jp93G5_VUSKKGdkXinbXzZw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

On Tue, 28 Jul 2026 at 12:57, Ayush Tiwari <ayushtiwari(dot)slg01(at)gmail(dot)com>
wrote:

> Hi,
>
> On Fri, 19 Jun 2026 at 06:43, Richard Guo <guofenglinux(at)gmail(dot)com> wrote:
>
>> 383eb21eb teaches the planner to convert "x NOT IN (SELECT ...)" to an
>> anti-join when both sides of the comparison are known to be non-null.
>> On the outer side we currently get that only from NOT NULL constraints
>> and the outer-join-aware-Var infrastructure.
>>
>> That misses a case: the left-hand column has no NOT NULL constraint,
>> but a qual forces it non-null anyway, as in "x IS NOT NULL AND x NOT
>> IN (...)" or "x > 0 AND x NOT IN (...)". We leave those as SubPlan
>> filters today, even though x clearly can't be NULL where the NOT IN is
>> evaluated.
>>
>> The attached patch proves the left-hand Var non-null from such a qual.
>> pull_up_sublinks_jointree_recurse collects the quals at or below the
>> NOT IN's jointree node (only those on rels not below the nullable side
>> of an outer join, so they really do filter the rows) and hands them to
>> convert_ANY_sublink_to_join, which checks them with
>> find_nonnullable_vars.
>>
>> Quals above the NOT IN's node could help in some cases too, but that's
>> a separate extension and I've left it as a follow-up. See the details
>> in the draft commit message.
>>
>
> Thanks for the patch!
>
> I did see one issue in this here's a statement for repro:
>
> CREATE TEMP TABLE outer_t (x int);
> CREATE TEMP TABLE inner_t (y int NOT NULL);
>
> INSERT INTO outer_t VALUES (NULL), (1);
> INSERT INTO inner_t VALUES (2);
>
> SELECT x
> FROM outer_t
> WHERE NOT (x = ANY('{}'::int[]))
> AND x NOT IN (SELECT y FROM inner_t);
>
> Apart from the above, your patch looked good to me.
>

I tried the patch on current master after Tom's SAOP fix. I may be
missing something, but I think there could be an issue when a NOT IN
left-hand expression contains an upper-level Var.

For example:

CREATE TEMP TABLE outer_t (a int);
CREATE TEMP TABLE local_t (a int);
CREATE TEMP TABLE inner_t (x int NOT NULL, y int NOT NULL);

INSERT INTO outer_t VALUES (NULL), (10);
INSERT INTO local_t VALUES (1);
INSERT INTO inner_t VALUES (1, 999);

SELECT o.a,
ARRAY(SELECT l.a
FROM local_t l
WHERE l.a IS NOT NULL
AND (l.a, o.a) NOT IN
(SELECT i.x, i.y FROM inner_t i)) AS vals
FROM outer_t o
ORDER BY o.a NULLS FIRST;

With the patch I get {1} for both outer rows, while without the
anti-join conversion the NULL row produces {}.

It looks like l.a and the upper-level o.a can have the same
varno/varattno, despite having different varlevelsup values. Could the
mbms_is_member() fallback therefore mistake the proof from
l.a IS NOT NULL as applying to o.a?

Would it make sense to restrict that fallback to level-zero Vars, for
example:

if (nonnullable_quals != NIL && IsA(expr, Var) &&
((Var *) expr)->varlevelsup == 0)

With that additional check, I get the expected result.

Regards,
Ayush

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Chao Li 2026-07-29 06:00:19 Re: Fix stale comment in parallel_vacuum_main().
Previous Message Amit Kapila 2026-07-29 05:28:34 Re: Fix "unexpected logical decoding status change" error; from concurrent logical decoding activation