Re: pull-up subquery if JOIN-ON contains refs to upper-query
Alena Rybakina <lena.ribackina@yandex.ru>
From: Alena Rybakina <lena.ribackina@yandex.ru>
To: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Cc: Ranier Vilela <ranier.vf@gmail.com>,
PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>,
Peter Petrov <p.petrov@postgrespro.ru>, David Rowley <dgrowleyml@gmail.com>
Date: 2025-10-10T11:15:58Z
Lists: pgsql-hackers
Attachments
- v6-0001-Enables-pull-up-of-EXISTS-subqueries-that-contain-IN.patch (text/x-patch) patch v6-0001
Hi! On 03.09.2025 00:07, Ilia Evdokimov wrote: > > Hi, > > I've reviewed this patch, and I have suggestion about the approach. > > Currently, the patch extends 'convert_EXISTS_sublick_to_join' with > quite complex logic (clause collection, volatile checks, rewriting > join quals, etc). While it works, the amount of branching and special > cases makes the function harder to follow. > > Looking at the logic, it seems that a large part of the complexity > comes from trying to directly adapt 'convert_EXISTS_sublink_to_join' > instead of factoring out a dedicated path. An alternative would be to > introduce a separate function > *'convert_EXISTS_sublink_to_lateral_join' *- with a similar API to > 'convert_ANY_sublink_to_join'. Such a function can focus only on the > EXISTS-to-join case, while keeping the existing function shorter and > easier to reason about. > > I even made some first rough sketches of this approach (not a finished > patch, just an outline). Of course, it would still need proper > adaptation, but I think it demonstrates that the overall structure can > be kept simpler. > > What do you think about refactoring in this direction? > > I think this approach isn’t fully correct. By forming a join between a subquery and the outer relation, you effectively force the optimizer to choose a Nested Loop join. Furthermore, it prevents the planner from exploring all join orders between the subquery’s tables and the outer relation, so we may miss a more optimal plan. With your patch, I consistently get the following plan. I even disabled nested loops to see whether the planner could switch to a Hash Join or Merge Join, but those aren’t applicable with lateral parameters in this pattern. CREATE TABLE ta (id int PRIMARY KEY, val int); INSERT INTO ta VALUES (1,1), (2,2); CREATE TABLE tb (id int PRIMARY KEY, aval int); INSERT INTO tb VALUES (1,1), (2,1); CREATE TABLE tc (id int PRIMARY KEY, aid int); INSERT INTO tc VALUES (3,5), (1,5); CREATE TABLE td (id int PRIMARY KEY, aid int); INSERT INTO td VALUES (1,6), (2,7), (3,8), (4,9); CREATE TABLE te (id int PRIMARY KEY, aid int); INSERT INTO te VALUES (5,6), (6,7), (7,8), (4,9), (1,1); SET enable_nestloop = OFF; EXPLAIN ANALYZE SELECT ta.id FROM ta WHERE EXISTS ( SELECT 1 FROM tb WHERE tb.id = ta.id AND EXISTS (SELECT 1 FROM tc WHERE tc.id = tb.id) ); QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Nested Loop Semi Join (cost=0.31..37017.50 rows=2260 width=4) (actual time=0.116..0.142 rows=2.00 loops=1) Disabled: true Buffers: local hit=9 -> Seq Scan on ta (cost=0.00..32.60 rows=2260 width=4) (actual time=0.037..0.039 rows=2.00 loops=1) Buffers: local hit=1 -> Nested Loop Semi Join (cost=0.31..16.36 rows=1 width=0) (actual time=0.045..0.046 rows=1.00 loops=2) Disabled: true Buffers: local hit=8 -> Index Only Scan using tb_pkey on tb (cost=0.15..8.17 rows=1 width=4) (actual time=0.030..0.030 rows=1.00 loops=2) Index Cond: (id = ta.id) Heap Fetches: 2 Index Searches: 2 Buffers: local hit=4 -> Index Only Scan using tc_pkey on tc (cost=0.15..8.17 rows=1 width=4) (actual time=0.010..0.010 rows=1.00 loops=2) Index Cond: (id = ta.id) Heap Fetches: 2 Index Searches: 2 Buffers: local hit=4 Planning Time: 0.539 ms Execution Time: 0.252 ms (20 rows) Anyway, thank you for the work and attention here - your feedback was useful! I’ve also rebased the patch on current master.