Re: Assert failure in try_nestloop_path()

From: Richard Guo <guofenglinux(at)gmail(dot)com>
To: Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>
Cc: Tender Wang <tndrwang(at)gmail(dot)com>, Pg Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: Re: Assert failure in try_nestloop_path()
Date: 2026-09-07 07:43:32
Message-ID: CAMbWs4_Ezb1Lnj7BqcTQSysC6B5UFZxfhib8xr6rqpu5uqr8dA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Mon, Sep 7, 2026 at 12:03 PM Richard Guo <guofenglinux(at)gmail(dot)com> wrote:
> Thanks for reviewing! I agree that back-patching into stable
> branches isn't worth the risk of plan changes, given the lack of field
> complaints. I've pushed this to master and v19.

I was curious whether there are other cases where we can end up with
duplicate qual clauses, so I added the attached Assert to verify that
the clauses to be enforced at a join or at a parameterized path's scan
contain no duplicate rinfo_serial, and the regression tests
immediately crashed :-O

One regression query that trips the Assert is:

explain (costs off)
select * from onek t1
left join onek t2 on t1.unique1 = t2.unique1
left join onek t3 on t2.unique1 = t3.unique1
left join onek t4 on t3.unique1 = t4.unique1 and t2.unique2 = t4.unique2;

It crashes in get_baserel_parampathinfo for base rel t4, whose
joininfo contains four clauses:

[0] serial=3 clause_relids={4 5 6} required={2 4 5 6} incompatible={3 7}
[1] serial=4 clause_relids={2 6} required={2 4 5 6} incompatible={3 7}
[2] serial=3 clause_relids={4 5 6} required={2 3 4 5 6} incompatible={7}
[3] serial=4 clause_relids={2 3 6} required={2 3 4 5 6} incompatible={7}

[0] and [2] are two clone variants of "t3.unique1 = t4.unique1".
Since the commuting outer join (relid 3) nulls no Var referenced by
this clause, the two variants are textually identical, differing only
in required_relids and incompatible_relids.

When t4 is probed with required_outer = {4, 5}, joinrelids is {4 5 6}.
Both variants are movable into the scan, and neither one's
incompatible_relids overlaps joinrelids, so both end up in
ppi_clauses.

It seems to me that something is wrong somewhere.

After a closer look, I don't think anything is wrong in
deconstruct_distribute_oj_quals. Both variants are needed for clause
selection at joins, where subbuild_joinrel_restrictlist checks
required_relids and incompatible_relids against the input relids. In
the normal join order, [0] is rejected because relid 3 appears in its
incompatible_relids, and [2] is the one applied; in the commuted
order, where t3/t4 join is performed below t1/t2 join, [2] is rejected
because relid 3 in its required_relids is not available, and [0] is
the one applied.

But a parameterized path cannot tell them apart. Since outer join 3
nulls no Var referenced by this clause, the parameterization looks
exactly the same whether that join is computed below the scan (the
normal join order, where [2] is the right variant) or above it (the
commuted order, where [0] is), so the same ParamPathInfo serves both
orders. For the same reason, either variant is correct in any join
order, so I think we should just enforce one of them and ignore the
rest? Thought?

- Richard

Attachment Content-Type Size
assert_no_duplicate_clause_serials.patch application/octet-stream 1.6 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Nikita Malakhov 2026-09-07 07:45:01 Re: Direct TOAST v2, faster, smaller and no migration needed
Previous Message Jelte Fennema-Nio 2026-09-07 07:31:37 Re: Python/pytest test framework take two