Re: DO NOT pull up a sublink when it has no join condition with the upper relation

From: Tender Wang <tndrwang(at)gmail(dot)com>
To: ld_zju <ld_zju(at)126(dot)com>
Cc: pgsql-bugs(at)lists(dot)postgresql(dot)org, Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>
Subject: Re: DO NOT pull up a sublink when it has no join condition with the upper relation
Date: 2026-07-31 02:52:55
Message-ID: CAHewXN=80HJz8fBiQpf9vFh1NK1vdEHMdSEb6BYV9toZikooPA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

ld_zju <ld_zju(at)126(dot)com> 于2026年7月31日周五 00:16写道:
>
> Hi,
>
> I've encountered a scenario where pulling up a sublink not only brings no benefit but actually degrades the final plan significantly.
>
> Here is the test case:
>
> create table t1(a int,b int,c int,d int);
> create table t2(a int,b int,c int,d int);
> create table t3(a int,b int,c int,d int);
> insert into t1 select i,i,i,i from generate_series(1,1000) i;
> insert into t2 select i,i,i,i from generate_series(1,1000) i;
> insert into t3 select i,i,i,i from generate_series(1,10) i;
>
> explain select * from t1 where exists(select 1 from t2 where t2.a in(select t3.a from t3 where t3.b=t1.b));
> QUERY PLAN
> -------------------------------------------------------------------
> Nested Loop Semi Join (cost=0.00..28418232.67 rows=925 width=16)
> Join Filter: (ANY (t2.a = (SubPlan any_1).col1))
> -> Seq Scan on t1 (cost=0.00..28.50 rows=1850 width=16)
> -> Materialize (cost=0.00..37.75 rows=1850 width=4)
> -> Seq Scan on t2 (cost=0.00..28.50 rows=1850 width=4)
> SubPlan any_1
> -> Seq Scan on t3 (cost=0.00..33.12 rows=9 width=4)
> Filter: (b = t1.b)
> (8 rows)
>
> The EXISTS sublink is pulled up and joined with t1 via a Nested Loop Semi Join. However, since there is no join condition between t1 and the sublink (the condition t3.b = t1.b is inside the subplan), this results in a Cartesian product between t1 and t2, followed by filtering through the subplan. With t1 and t2 both having 1000 rows, this produces a large intermediate result set (1,000,000 rows) when the actual result set is much smaller.
>
> Would it be possible that the sublink is pulled up only when it has any join conditions with the upper relation? If no such conditions exist, a Cartesian product is likely and pulling up should be avoided.
>
> Any thoughts or suggestions would be appreciated!

You can add "offset 0" into the subquery; then the plan should be what you want.
postgres=# explain select * from t1 where exists(select 1 from t2
where t2.a in(select t1.b from t3 where t3.b=t1.b) offset 0);
QUERY PLAN
------------------------------------------------------------------
Seq Scan on t1 (cost=0.00..19651.00 rows=500 width=16)
Filter: EXISTS(SubPlan exists_1)
SubPlan exists_1
-> Nested Loop Semi Join (cost=0.00..19.64 rows=1 width=4)
-> Seq Scan on t2 (cost=0.00..18.50 rows=1 width=0)
Filter: (a = t1.b)
-> Seq Scan on t3 (cost=0.00..1.12 rows=1 width=0)
Filter: (b = t1.b)
(8 rows)

And the Execution Time: 178.877 ms; without "offset 0", it is 4048.970
ms on my machine.

In convert_EXISTS_sublink_to_join(), we have:
/*
* On the other hand, the WHERE clause must contain some Vars of the
* parent query, else it's not gonna be a join.
*/
if (!contain_vars_of_level(whereClause, 1))
return NULL;

When we recurse into the third sublink in
contain_vars_of_level_walker(), the levelsup was +1(i.e. 2)
t1.b in "t3.b = t1.b" is Var [varno=1 varattno=2 vartype=23
varlevelsup=2 varreturningtype=VAR_RETURNING_DEFAULT varnosyn=1
varattnosyn=2]
You can see that varlevelsup is 2, so
contain_vars_of_level(whereClause, 1) returns true. Then the sublink
is pulled up.

I made some attempts.

#1
We can't simply remove the"(*sublevels_up)++; " in
contain_vars_of_level_walker(); because some other places also call
this function.
If you do this, the regression will crash.
#2
I rewrote a separate version based on the current implementation
specifically for SubLink pull-up. My goal was simply to see whether it
would cause any regression test failures.
The attached is my test. It's only for testing.
To my surprise, all the regression tests passed.

I'm not sure it is a bug. The code was committed 17 years ago by Tom.
And I'm not sure you're the first to report this issue.
I feel that in most cases, the second query will refer to the top
query's column, and the third query will refer to the second query's
column.

--
Thanks,
Tender Wang

Attachment Content-Type Size
0001-Only-test-for-sublink-pullup.patch application/octet-stream 3.8 KB

In response to

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message David Rowley 2026-07-31 05:24:31 Re: Hash Semi Join 5,000-50,000x slower on PG18 vs PG17 with 10+ equality columns and NULL values (identical plan, no spill)
Previous Message David Rowley 2026-07-31 01:20:17 Re: BUG #19579: Wrong results regression