Performance regression caused by SubLink pull-up

From: Richard Guo <guofenglinux(at)gmail(dot)com>
To: Pg Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Cc: ld_zju <ld_zju(at)126(dot)com>, tender wang <tndrwang(at)gmail(dot)com>
Subject: Performance regression caused by SubLink pull-up
Date: 2026-08-12 13:15:56
Message-ID: CAMbWs4_mRGESi-=ZpYLJbgSOZ+pak11HqiM3aF04_fe3z1=3rQ@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

In [1] it was reported that pulling up a SubLink in the query below
causes a severe performance regression.

select * from t1 where exists
(select 1 from t2 where t2.a in
(select t3.a from t3 where t3.b = t1.b));

I tested it with the data from the report. If we do not pull up the
outer SubLink, the Execution Time: 6.020 ms. If we do, as master
does, the Execution Time: 3380.646 ms.

So I think this is more than a missed optimization; it is an active
pessimization.

I had a look into this issue, and here is what I found.

Pulling up the outer SubLink takes away the inner one's own chance to
be pulled up. While the inner SubLink sits inside the subquery, the
outer query's rels are a level further up, so only t2 need be in
available_rels, and it converts to a semijoin there. Once the outer
SubLink is pulled up, the inner one lands in the top-level qual
referencing both t1 and t2; pull_up_sublinks_qual_recurse has just two
places to put a new join, the semijoin's LHS and its RHS, and a
SubLink referencing both fits neither. So it stays a SubPlan, and the
join it used to get is gone.

If we do not pull up the outer SubLink, the plan is in form:

Form 1: A WHERE EXISTS SubPlan[ B SEMI JOIN C ON P ]

Write a=|A|, b=|B|, c=|C|, and P = Pbc AND Pabc, where Pbc references
only B and/or C, whereas Pabc also references A.

Since there is a valid Pbc, the cost of Form 1 is:

Cost1 = O(a*(b+c)) B and C joined, once per A row

If we pull up the outer sublink, as master does, the plan is in form:

Form 2: A SEMI JOIN B ON SubPlan[C]

With beta(x) the position of the first qualifying B row and N = sum of
beta(x) <= a*b the number of pairs reached:

hashed SubPlan: c + N built once, O(1) per probe
rescanned SubPlan: N*s s in [1,c]

In the reported case the SubPlan is not hashable, so the cost is:

Cost2 = O(a*b*c)

and it really does reach that: the SubPlan is executed 990055 times.

Regarding the fix, I think one way is to teach the planner to pull up
sublinks that reference both sides of a new semijoin.

Before:

SELECT * FROM A WHERE EXISTS
(SELECT 1 FROM B WHERE EXISTS (SELECT 1 FROM C WHERE P));

After:

SELECT * FROM A WHERE EXISTS
(SELECT 1 FROM B, C WHERE P);

The first asks: is there a B row for which some C row satisfies P?

The second asks: is there a (B, C) pair satisfying P?

So they ask the same question.

Duplicates can't matter because we're deciding whether to emit rows of
A, not of B or C. How many pairs qualify is invisible in the output.

However, this transformation is not always a win. If there is no join
condition between B and C, the B/C join is a cross product, which can
be worse than the SubPlan it displaces, especially when that SubPlan
can be hashed. So I'm thinking we do this only when Pbc actually
relates B to C, in which case we get this form:

Form 3: A SEMI JOIN (B JOIN C ON Pbc) ON Pabc

Write R = |B JOIN C ON Pbc|, then the cost is:

W = producing the join. At least R, since those rows must be
emitted; b + c + R for a hash join, which is what a hashable
Pbc gives.
Cost3 = W + a Pabc gives a clean A | {B,C} split, so the RHS
is built once and probed a times

R can be as small as O(b), when the C side of Pbc is key-like, and as
large as b*c when it is not, on the data alone. In the first case
Cost3 = O(a+b+c).

Which end R falls at depends on the selectivity of Pbc, and that isn't
knowable this early, so the test is a heuristic.

I ran this with the same data, and the Execution Time: 0.605 ms.

Attached is the patch implementing this. Any thoughts?

[1] https://postgr.es/m/13beb150.7b23.19fb3cf6efc.Coremail.ld_zju@126.com

- Richard

Attachment Content-Type Size
v1-0001-Pull-up-sublinks-that-reference-both-sides-of-a-n.patch application/octet-stream 32.2 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Hüseyin Demir 2026-08-12 13:28:13 Re: [PATCH] pg_upgrade: add --initdb option to create the new cluster automatically
Previous Message Nikhil Sontakke 2026-08-12 12:56:09 Re: Logical replication row filter loses unchanged toasted columns