ERROR: failed to assign all NestLoopParams to plan nodes

From: Richard Guo <guofenglinux(at)gmail(dot)com>
To: Pg Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: ERROR: failed to assign all NestLoopParams to plan nodes
Date: 2026-10-13 10:05:31
Message-ID: CAMbWs4-fcAULTpWthZMHGOFK1J+Hz=Dvr1TO-M20cC317rOGOA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

The query below hits $subject:

create table t (a int);
insert into t select generate_series(1, 1000);
create table o (a int);
insert into o values (1);
analyze t, o;

select v.k
from (select 1) d
left join (select t1.a, 1 from t t1, o t2 left join o t3 on true)
s(x, y) on true
left join lateral (select s.y from o t4) v(k) on true
where v.k > s.x;
ERROR: failed to assign all NestLoopParams to plan nodes

The PHV for s.y has ph_eval_at {t1, t2, t3, t2/t3 join}, and the PHV
for v.k is evaluated at t4 and references it laterally. The plan we
pick is

Nested Loop
-> Nested Loop Left Join
-> Seq Scan on o t2
-> Seq Scan on o t3
-> Nested Loop
-> Seq Scan on t t1
-> Seq Scan on o t4
Filter: ((1) > t1.a)

So the lower nestloop has to supply the s.y PHV, computing it at t1
with t2 and t3 passed in from above. identify_current_nestloop_params
is fine with that as long as ph_eval_at is covered by the lefthand
rels plus the join's required-outer rels. The trouble is that the
required-outer rels here are only {t2, t3}. The outer-join relid is
missing, because create_lateral_join_info() drops outer-join relids
from the lateral dependencies it gets from ph_lateral. So the check
fails and nobody claims the parameter. If there is another nestloop
on top that happens to cover ph_eval_at, it claims the parameter
instead and we get "plan should not reference subplan's variable".

The simplest fix I can think of is to also count the outer joins that
the outer rels in scope have already computed as req_outer in
create_nestloop_plan(), though this is another band-aid here. See
0001.

While digging into this, I noticed that create_lateral_join_info()
treats outer-join relids differently in its two loops. For a PHV in a
rel's lateral_vars it adds the whole ph_eval_at, outer joins and all.
For ph_lateral it strips them, since efeb12ef0.

I don't think either is wrong, but ISTM the line is drawn in the wrong
place. I think what matters here is who consumes the PHV. A subquery
is planned separately and references the PHV through a single Param,
so some nestloop must supply the PHV's value as a whole, and that is
only possible once the outer joins in its ph_eval_at have been formed.

A function, VALUES or tablesample scan doesn't need the PHV as a
whole. When the outer joins in its ph_eval_at have not been formed
yet, replace_nestloop_params() recurses into the PHV's expression and
parameterizes the Vars inside, so the scan computes the expression
itself. That gives the same answer, because the scan can only be
joined below such an outer join via identity 3, which requires its
join clause to be strict for the nullable side. So the rows the outer
join would null-extend cannot match it anyway. We already rely on
this for PHVs that reference other PHVs laterally, since efeb12ef0.
So for those rels the outer-join relids buy us nothing, and they cost
us join orders that identity 3 allows. For example, with the same
tables:

explain (costs off)
select * from o d
left join (select coalesce(t2.a, 0) as x, t2.a as q
from t t1 left join o t2 on true) ss on true
left join lateral generate_series(ss.x, 10) f on ss.q = 1;
QUERY PLAN
------------------------------------------------------
Nested Loop Left Join
-> Seq Scan on o d
-> Nested Loop Left Join
Join Filter: (t2.a = 1)
-> Nested Loop Left Join
-> Seq Scan on t t1
-> Materialize
-> Seq Scan on o t2
-> Memoize
Cache Key: (COALESCE(t2.a, 0))
Cache Mode: binary
-> Function Scan on generate_series f
(12 rows)

Here f can only be joined above the t1/t2 join. If we remove the
t1/t2 ojrelid from the lateral_relids, f would be able to be joined to
t2 first:

Nested Loop Left Join
-> Seq Scan on o d
-> Nested Loop Left Join
-> Seq Scan on t t1
-> Materialize
-> Nested Loop Left Join
Join Filter: (t2.a = 1)
-> Seq Scan on o t2
-> Function Scan on generate_series f
(9 rows)

The attached 0002 does that. It's not a bug fix, so it's for master
only.

Thoughts?

- Richard

Attachment Content-Type Size
v1-0001-Fix-nestloop-parameter-assignment-for-PHVs-evalua.patch application/octet-stream 5.4 KB
v1-0002-Don-t-require-outer-joins-for-lateral-PHV-referen.patch application/octet-stream 5.6 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message shveta malik 2026-10-13 10:28:52 Re: Proposal: Conflict log history table for Logical Replication
Previous Message Dilip Kumar 2026-10-13 10:03:54 Re: Proposal: Conflict log history table for Logical Replication