From 6fd05d7d10e7ea8ccbb9ad56ffb25128b28b2917 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Fri, 31 Jul 2026 15:30:16 +0900
Subject: [PATCH v1] Pull up sublinks that reference both sides of a new
 semijoin

When pull_up_sublinks turns an ANY or EXISTS SubLink into a semijoin,
it recurses into the quals it pulled up, offering two places to insert
further joins: the new semijoin's LHS, for SubLinks referencing only
the outer rels, and its RHS, for SubLinks referencing only the
pulled-up ones.  A SubLink referencing rels on both sides fits
neither, so we left it behind as a SubPlan.  That can be far worse
than never having pulled up the outer SubLink at all; in the case that
prompted this, the SubPlan was rescanned once per row pair of the
semijoin.

Since a semijoin only cares whether its RHS yields any matching row,
and its own qual may reference both sides, such a SubLink can instead
be pulled up into the RHS with its join qual handed back to the
semijoin's own qual:

        A SEMI JOIN (B SEMI JOIN C ON P)  ==
        A SEMI JOIN (B JOIN C) ON P

The duplicates the plain join may produce don't change the result.

Do this only when some conjunct of P relates C to the rels already in
the RHS.  Without one the RHS is a cross product, while the SubPlan we
would displace does no worse, and much better when it can be hashed
and reads C just once.  With such a conjunct, what the join costs
depends on selectivities not knowable this early, so the test is a
heuristic: it declines the cases that are bad by construction, not all
the bad ones.

It doesn't apply underneath a NOT, where the antijoin replacing the
SubLink would have to keep its own qual, which then couldn't reference
A.  The same reasoning would allow it in an antijoin's RHS, but the
NOT cases pass down only one link, and no rels for the antijoin's LHS,
so they can't reach this code as it stands; leave that for another
day.
---
 src/backend/optimizer/prep/prepjointree.c | 144 ++++++-
 src/test/regress/expected/subselect.out   | 468 +++++++++++++++++++++-
 src/test/regress/sql/subselect.sql        | 152 +++++++
 3 files changed, 756 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 72d39ba30fd..69d08fab872 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -113,6 +113,8 @@ static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 										   Node **jtlink1, Relids available_rels1,
 										   Node **jtlink2, Relids available_rels2);
+static bool sublink_has_join_clause(SubLink *sublink, Relids rels);
+static bool qual_has_join_clause(Node *qual, int levelsup, Relids rels);
 static Node *pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 										JoinExpr *lowest_outer_join,
 										AppendRelInfo *containing_appendrel);
@@ -845,11 +847,14 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
  * denotes the relations present underneath jtlink1).  Optionally, jtlink2 can
  * point to a second link where new JoinExprs should be inserted if they
  * reference available_rels2 (pass NULL for both those arguments if not used).
- * Note that SubLinks referencing both sets of variables cannot be optimized.
  * If we find multiple pull-up-able SubLinks, they'll get stacked onto jtlink1
  * and/or jtlink2 in the order we encounter them.  We rely on subsequent
  * optimization to rearrange the stack if appropriate.
  *
+ * A SubLink referencing both sets fits neither link, but as a last resort it
+ * can still be pulled up into the RHS of a semijoin we just built; see the
+ * comment on that in the ANY_SUBLINK case below.
+ *
  * Returns the replacement qual node, or NULL if the qual should be removed.
  */
 static Node *
@@ -932,6 +937,56 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 				/* Return NULL representing constant TRUE */
 				return NULL;
 			}
+
+			/*
+			 * Neither link would take it, typically because it references
+			 * rels on both sides.  As a last resort, if jtlink1 and jtlink2
+			 * are the larg and rarg of a JOIN_SEMI JoinExpr we just built,
+			 * pull it up into that semijoin's RHS, handing its join qual back
+			 * to become part of the semijoin's own qual.  The duplicates the
+			 * plain join may produce don't matter to a semijoin.  Only worth
+			 * doing if it joins to what's in the RHS already.
+			 */
+			if (available_rels2 != NULL &&
+				sublink_has_join_clause(sublink, available_rels2) &&
+				(j = convert_ANY_sublink_to_join(root, sublink, false,
+												 bms_union(available_rels1,
+														   available_rels2))) != NULL)
+			{
+				Node	   *rarg;
+
+				/* Recursively process pulled-up jointree nodes */
+				rarg = pull_up_sublinks_jointree_recurse(root, j->rarg,
+														 &child_rels);
+
+				/*
+				 * Add it to the semijoin's RHS.  A quals-less FromExpr is
+				 * just a cross product, so extend its fromlist if the RHS
+				 * already is one; else build a new FromExpr.
+				 */
+				if (IsA(*jtlink2, FromExpr) &&
+					((FromExpr *) *jtlink2)->quals == NULL)
+				{
+					FromExpr   *f = (FromExpr *) *jtlink2;
+
+					f->fromlist = lappend(f->fromlist, rarg);
+				}
+				else
+					*jtlink2 = (Node *) makeFromExpr(list_make2(*jtlink2,
+																rarg), NULL);
+
+				/*
+				 * Hand the qual back to be put where the SubLink was, that
+				 * is, to become part of the semijoin's own qual.  The rels we
+				 * just pulled up are underneath jtlink2 now, so they're
+				 * available to anything inserted into it.
+				 */
+				return pull_up_sublinks_qual_recurse(root, j->quals,
+													 jtlink1, available_rels1,
+													 jtlink2,
+													 bms_union(available_rels2,
+															   child_rels));
+			}
 		}
 		else if (sublink->subLinkType == EXISTS_SUBLINK)
 		{
@@ -986,6 +1041,36 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 				/* Return NULL representing constant TRUE */
 				return NULL;
 			}
+
+			/* Same last-resort treatment as for ANY SubLinks */
+			if (available_rels2 != NULL &&
+				sublink_has_join_clause(sublink, available_rels2) &&
+				(j = convert_EXISTS_sublink_to_join(root, sublink, false,
+													bms_union(available_rels1,
+															  available_rels2))) != NULL)
+			{
+				Node	   *rarg;
+
+				rarg = pull_up_sublinks_jointree_recurse(root, j->rarg,
+														 &child_rels);
+
+				if (IsA(*jtlink2, FromExpr) &&
+					((FromExpr *) *jtlink2)->quals == NULL)
+				{
+					FromExpr   *f = (FromExpr *) *jtlink2;
+
+					f->fromlist = lappend(f->fromlist, rarg);
+				}
+				else
+					*jtlink2 = (Node *) makeFromExpr(list_make2(*jtlink2,
+																rarg), NULL);
+
+				return pull_up_sublinks_qual_recurse(root, j->quals,
+													 jtlink1, available_rels1,
+													 jtlink2,
+													 bms_union(available_rels2,
+															   child_rels));
+			}
 		}
 		/* Else return it unmodified */
 		return node;
@@ -1143,6 +1228,63 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 	return node;
 }
 
+/*
+ * Has this SubLink a clause joining it to rels?
+ *
+ * Without one, pulling it up alongside rels makes only a cross product, which
+ * is worse than leaving it a SubPlan; see pull_up_sublinks_qual_recurse.
+ */
+static bool
+sublink_has_join_clause(SubLink *sublink, Relids rels)
+{
+	Query	   *subselect = (Query *) sublink->subselect;
+
+	/*
+	 * Either the test expression or the sub-select's own WHERE clause will
+	 * do.
+	 */
+	return qual_has_join_clause(sublink->testexpr, 0, rels) ||
+		qual_has_join_clause(subselect->jointree->quals, 1, rels);
+}
+
+/*
+ * Does qual contain a clause joining the sub-select to rels, that is, one
+ * referencing the sub-select and, levelsup levels up, nothing outside rels?
+ *
+ * Quals aren't AND-flat this early, so we have to recurse.
+ */
+static bool
+qual_has_join_clause(Node *qual, int levelsup, Relids rels)
+{
+	Relids		varnos;
+
+	if (qual == NULL)
+		return false;
+
+	if (is_andclause(qual))
+	{
+		ListCell   *lc;
+
+		foreach(lc, ((BoolExpr *) qual)->args)
+		{
+			if (qual_has_join_clause((Node *) lfirst(lc), levelsup, rels))
+				return true;
+		}
+		return false;
+	}
+
+	/*
+	 * When qual is the sub-select's own WHERE clause, not every clause in it
+	 * need mention the sub-select; one that doesn't only restricts rels.
+	 */
+	if (levelsup > 0 && !contain_vars_of_level(qual, 0))
+		return false;
+
+	varnos = pull_varnos_of_level(NULL, qual, levelsup);
+
+	return !bms_is_empty(varnos) && bms_is_subset(varnos, rels);
+}
+
 /*
  * preprocess_function_rtes
  *		Constant-simplify any FUNCTION RTEs in the FROM clause, and then
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ef49b5756a7..298d0920c06 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -3120,16 +3120,16 @@ select * from tenk1 A where exists
 (select 1 from tenk2 B
 where A.hundred in (select C.hundred FROM tenk2 C
 WHERE c.odd = b.odd));
-                       QUERY PLAN                        
----------------------------------------------------------
+                         QUERY PLAN                          
+-------------------------------------------------------------
  Nested Loop Semi Join
-   Join Filter: (ANY (a.hundred = (SubPlan any_1).col1))
    ->  Seq Scan on tenk1 a
-   ->  Materialize
+   ->  Hash Join
+         Hash Cond: (b.odd = c.odd)
          ->  Seq Scan on tenk2 b
-   SubPlan any_1
-     ->  Seq Scan on tenk2 c
-           Filter: (odd = b.odd)
+         ->  Hash
+               ->  Index Scan using tenk2_hundred on tenk2 c
+                     Index Cond: (hundred = a.hundred)
 (8 rows)
 
 -- we should only try to pull up the sublink into RHS of a left join
@@ -3230,6 +3230,460 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                                                Filter: (odd = b.odd)
 (17 rows)
 
+--
+-- Pull up sublinks that reference both sides of a semijoin we just made
+--
+-- the ANY sublink references both A and B; we pull it up into the RHS
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.hundred = A.odd)) order by 1;
+                                    QUERY PLAN                                    
+----------------------------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Hash Join
+         Hash Cond: (a.odd = c.hundred)
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Hash
+               ->  HashAggregate
+                     Group Key: c.hundred
+                     ->  Hash Join
+                           Hash Cond: (c.unique1 = b.unique1)
+                           ->  Seq Scan on onek c
+                           ->  Hash
+                                 ->  Index Only Scan using onek_unique1 on onek b
+(16 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.hundred = A.odd)) order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+(5 rows)
+
+-- likewise for a nested EXISTS
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and C.unique1 = B.unique1)) order by 1;
+                                    QUERY PLAN                                    
+----------------------------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Hash Join
+         Hash Cond: (a.odd = c.hundred)
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Hash
+               ->  HashAggregate
+                     Group Key: c.hundred
+                     ->  Hash Join
+                           Hash Cond: (c.unique1 = b.unique1)
+                           ->  Seq Scan on onek c
+                           ->  Hash
+                                 ->  Index Only Scan using onek_unique1 on onek b
+(16 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and C.unique1 = B.unique1)) order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+(5 rows)
+
+-- rels pulled into the RHS must be available to sublinks pulled up after
+-- them
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and exists (select 1 from onek D
+where D.unique1 = C.unique1 and D.unique2 = B.unique2))) order by 1;
+                                    QUERY PLAN                                    
+----------------------------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Hash Join
+         Hash Cond: (a.odd = c.hundred)
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Hash
+               ->  HashAggregate
+                     Group Key: c.hundred
+                     ->  Hash Join
+                           Hash Cond: (d.unique2 = b.unique2)
+                           ->  Hash Join
+                                 Hash Cond: (c.unique1 = d.unique1)
+                                 ->  Seq Scan on onek c
+                                 ->  Hash
+                                       ->  HashAggregate
+                                             Group Key: d.unique1, d.unique2
+                                             ->  Seq Scan on onek d
+                           ->  Hash
+                                 ->  Index Only Scan using onek_unique2 on onek b
+(22 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and exists (select 1 from onek D
+where D.unique1 = C.unique1 and D.unique2 = B.unique2))) order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+(5 rows)
+
+-- the qual we hand back can itself hold a sublink referencing both sides
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = B.unique1 and exists (select 1 from onek D
+where D.hundred = A.odd and D.unique2 = C.unique2))) order by 1;
+                                       QUERY PLAN                                       
+----------------------------------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Hash Join
+         Hash Cond: (a.odd = d.hundred)
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Hash
+               ->  HashAggregate
+                     Group Key: d.hundred
+                     ->  Hash Join
+                           Hash Cond: (c.unique2 = d.unique2)
+                           ->  Hash Join
+                                 Hash Cond: (c.unique1 = b.unique1)
+                                 ->  Seq Scan on onek c
+                                 ->  Hash
+                                       ->  Index Only Scan using onek_unique1 on onek b
+                           ->  Hash
+                                 ->  Seq Scan on onek d
+(20 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = B.unique1 and exists (select 1 from onek D
+where D.hundred = A.odd and D.unique2 = C.unique2))) order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+(5 rows)
+
+-- the pulled-up subquery can't always be flattened; then it stays a lateral
+-- scan parameterized by the semijoin's LHS.
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd offset 0)) order by 1;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Nested Loop Semi Join
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Hash Join
+               Hash Cond: (b.unique1 = c.unique1)
+               ->  Index Only Scan using onek_unique1 on onek b
+               ->  Hash
+                     ->  Seq Scan on onek c
+                           Filter: (odd = a.odd)
+(13 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd offset 0)) order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+       5 |  10
+       6 |  12
+       7 |  14
+       8 |  16
+       9 |  18
+(10 rows)
+
+-- we don't do this when the enclosing join is an antijoin, although the
+-- same reasoning would hold there
+explain (costs off)
+select * from tenk1 A where not exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd));
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Anti Join
+   Join Filter: (ANY (b.unique1 = (SubPlan any_1).col1))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Index Only Scan using onek_unique1 on onek b
+   SubPlan any_1
+     ->  Seq Scan on onek c
+           Filter: (odd = a.odd)
+(8 rows)
+
+-- and we can't when the sublink itself is negated: the antijoin replacing it
+-- would have to keep its own qual, which then couldn't reference A
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where not exists (select 1 from onek C
+where C.odd = A.odd and C.hundred = B.hundred));
+                                                       QUERY PLAN                                                       
+------------------------------------------------------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: (NOT (ANY ((a.odd = (hashed SubPlan exists_2).col1) AND (b.hundred = (hashed SubPlan exists_2).col2))))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Index Only Scan using onek_hundred on onek b
+   SubPlan exists_2
+     ->  Seq Scan on onek c
+(7 rows)
+
+-- nor when the sublink isn't a top-level clause of the semijoin's qual
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where B.odd = A.odd or B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd));
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: ((b.odd = a.odd) OR (ANY (b.unique1 = (SubPlan any_1).col1)))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Seq Scan on onek b
+   SubPlan any_1
+     ->  Seq Scan on onek c
+           Filter: (odd = a.odd)
+(8 rows)
+
+-- nor when nothing in the sublink's qual joins it to the rels already in
+-- the semijoin's RHS
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where A.odd + B.odd in (select C.unique1 from onek C));
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: (ANY ((a.odd + b.odd) = (hashed SubPlan any_1).col1))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Seq Scan on onek b
+   SubPlan any_1
+     ->  Index Only Scan using onek_unique1 on onek c
+(7 rows)
+
+-- likewise for an EXISTS sublink, which can be hashed too, by way of
+-- convert_EXISTS_to_ANY
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = A.odd + B.odd));
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: (ANY ((a.odd + b.odd) = (hashed SubPlan exists_2).col1))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Seq Scan on onek b
+   SubPlan exists_2
+     ->  Index Only Scan using onek_unique1 on onek c
+(7 rows)
+
+-- a clause that doesn't mention the sub-select only restricts the RHS, so
+-- it doesn't count either
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where B.hundred = 5 and C.unique1 = A.odd + B.odd));
+                                                         QUERY PLAN                                                         
+----------------------------------------------------------------------------------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: (ANY ((b.hundred = (hashed SubPlan exists_2).col1) AND ((a.odd + b.odd) = (hashed SubPlan exists_2).col2)))
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Seq Scan on onek b
+   SubPlan exists_2
+     ->  Index Only Scan using onek_unique1 on onek c
+(7 rows)
+
+-- but one clause that does is enough, alongside another that doesn't
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where (A.odd + B.odd, B.unique2) in (select C.unique1, C.unique2 from onek C));
+                    QUERY PLAN                    
+--------------------------------------------------
+ Nested Loop Semi Join
+   Join Filter: ((a.odd + b.odd) = c.unique1)
+   ->  Seq Scan on tenk1 a
+   ->  Materialize
+         ->  Hash Join
+               Hash Cond: (b.unique2 = c.unique2)
+               ->  Seq Scan on onek b
+               ->  Hash
+                     ->  Seq Scan on onek c
+(9 rows)
+
+-- the sub-select's own WHERE clause serves as well as the test expression,
+-- becoming a join clause once the sub-select is flattened
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where A.odd in (select C.unique1 from onek C where C.unique2 = B.unique2))
+order by 1;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Sort
+   Sort Key: a.unique1
+   ->  Nested Loop Semi Join
+         ->  Bitmap Heap Scan on tenk1 a
+               Recheck Cond: (unique1 < 20)
+               ->  Bitmap Index Scan on tenk1_unique1
+                     Index Cond: (unique1 < 20)
+         ->  Nested Loop
+               ->  Index Scan using onek_unique1 on onek c
+                     Index Cond: (unique1 = a.odd)
+               ->  Index Only Scan using onek_unique2 on onek b
+                     Index Cond: (unique2 = c.unique2)
+(12 rows)
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where A.odd in (select C.unique1 from onek C where C.unique2 = B.unique2))
+order by 1;
+ unique1 | odd 
+---------+-----
+       0 |   0
+       1 |   2
+       2 |   4
+       3 |   6
+       4 |   8
+       5 |  10
+       6 |  12
+       7 |  14
+       8 |  16
+       9 |  18
+      10 |  20
+      11 |  22
+      12 |  24
+      13 |  26
+      14 |  28
+      15 |  30
+      16 |  32
+      17 |  34
+      18 |  36
+      19 |  38
+(20 rows)
+
+-- it's fine for all this to happen inside the nullable side of an outer
+-- join, since nothing gets moved out of that side
+explain (costs off)
+select X.unique1, A.unique1 from tenk1 X left join tenk1 A
+on X.unique1 = A.unique1
+and exists (select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd))
+where X.unique1 < 20 order by 1;
+                                       QUERY PLAN                                       
+----------------------------------------------------------------------------------------
+ Sort
+   Sort Key: x.unique1
+   ->  Hash Right Join
+         Hash Cond: (a.unique1 = x.unique1)
+         ->  Hash Join
+               Hash Cond: (a.odd = c.odd)
+               ->  Seq Scan on tenk1 a
+               ->  Hash
+                     ->  HashAggregate
+                           Group Key: c.odd
+                           ->  Hash Join
+                                 Hash Cond: (c.unique1 = b.unique1)
+                                 ->  Seq Scan on onek c
+                                 ->  Hash
+                                       ->  Index Only Scan using onek_unique1 on onek b
+         ->  Hash
+               ->  Index Only Scan using tenk1_unique1 on tenk1 x
+                     Index Cond: (unique1 < 20)
+(18 rows)
+
+select X.unique1, A.unique1 from tenk1 X left join tenk1 A
+on X.unique1 = A.unique1
+and exists (select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd))
+where X.unique1 < 20 order by 1;
+ unique1 | unique1 
+---------+---------
+       0 |       0
+       1 |       1
+       2 |       2
+       3 |       3
+       4 |       4
+       5 |       5
+       6 |       6
+       7 |       7
+       8 |       8
+       9 |       9
+      10 |        
+      11 |        
+      12 |        
+      13 |        
+      14 |        
+      15 |        
+      16 |        
+      17 |        
+      18 |        
+      19 |        
+(20 rows)
+
 --
 -- Test VALUES to ARRAY (VtA) transformation
 --
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index 0b18e0132aa..83be650642c 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -1454,6 +1454,158 @@ explain (costs off)
 SELECT * FROM tenk1 A LEFT JOIN tenk2 B
 ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
 
+--
+-- Pull up sublinks that reference both sides of a semijoin we just made
+--
+
+-- the ANY sublink references both A and B; we pull it up into the RHS
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.hundred = A.odd)) order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.hundred = A.odd)) order by 1;
+
+-- likewise for a nested EXISTS
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and C.unique1 = B.unique1)) order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and C.unique1 = B.unique1)) order by 1;
+
+-- rels pulled into the RHS must be available to sublinks pulled up after
+-- them
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and exists (select 1 from onek D
+where D.unique1 = C.unique1 and D.unique2 = B.unique2))) order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.hundred = A.odd and exists (select 1 from onek D
+where D.unique1 = C.unique1 and D.unique2 = B.unique2))) order by 1;
+
+-- the qual we hand back can itself hold a sublink referencing both sides
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = B.unique1 and exists (select 1 from onek D
+where D.hundred = A.odd and D.unique2 = C.unique2))) order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = B.unique1 and exists (select 1 from onek D
+where D.hundred = A.odd and D.unique2 = C.unique2))) order by 1;
+
+-- the pulled-up subquery can't always be flattened; then it stays a lateral
+-- scan parameterized by the semijoin's LHS.
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd offset 0)) order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd offset 0)) order by 1;
+
+-- we don't do this when the enclosing join is an antijoin, although the
+-- same reasoning would hold there
+explain (costs off)
+select * from tenk1 A where not exists
+(select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd));
+
+-- and we can't when the sublink itself is negated: the antijoin replacing it
+-- would have to keep its own qual, which then couldn't reference A
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where not exists (select 1 from onek C
+where C.odd = A.odd and C.hundred = B.hundred));
+
+-- nor when the sublink isn't a top-level clause of the semijoin's qual
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where B.odd = A.odd or B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd));
+
+-- nor when nothing in the sublink's qual joins it to the rels already in
+-- the semijoin's RHS
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where A.odd + B.odd in (select C.unique1 from onek C));
+
+-- likewise for an EXISTS sublink, which can be hashed too, by way of
+-- convert_EXISTS_to_ANY
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where C.unique1 = A.odd + B.odd));
+
+-- a clause that doesn't mention the sub-select only restricts the RHS, so
+-- it doesn't count either
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where exists (select 1 from onek C
+where B.hundred = 5 and C.unique1 = A.odd + B.odd));
+
+-- but one clause that does is enough, alongside another that doesn't
+explain (costs off)
+select * from tenk1 A where exists
+(select 1 from onek B
+where (A.odd + B.odd, B.unique2) in (select C.unique1, C.unique2 from onek C));
+
+-- the sub-select's own WHERE clause serves as well as the test expression,
+-- becoming a join clause once the sub-select is flattened
+explain (costs off)
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where A.odd in (select C.unique1 from onek C where C.unique2 = B.unique2))
+order by 1;
+
+select A.unique1, A.odd from tenk1 A where A.unique1 < 20 and exists
+(select 1 from onek B
+where A.odd in (select C.unique1 from onek C where C.unique2 = B.unique2))
+order by 1;
+
+-- it's fine for all this to happen inside the nullable side of an outer
+-- join, since nothing gets moved out of that side
+explain (costs off)
+select X.unique1, A.unique1 from tenk1 X left join tenk1 A
+on X.unique1 = A.unique1
+and exists (select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd))
+where X.unique1 < 20 order by 1;
+
+select X.unique1, A.unique1 from tenk1 X left join tenk1 A
+on X.unique1 = A.unique1
+and exists (select 1 from onek B
+where B.unique1 in (select C.unique1 from onek C
+where C.odd = A.odd))
+where X.unique1 < 20 order by 1;
+
 --
 -- Test VALUES to ARRAY (VtA) transformation
 --
-- 
2.37.1 (Apple Git-137.1)

