From d37eb5003b1fcbf25355007821f06c41d23d461c Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 6 Jun 2018 15:58:49 +0300
Subject: [PATCH 1/2] Push down join quals into lateral subqueries.

We don't normally push down join quals into subqueries, because that would
require creating parameterized plans. However, if the plan is already
parameterized because it's LATERAL, we might as well push down any
additional join quals, that refer the same relations that are already
referenced within the subquery.

This changes the behavior of the sublevels_up parameters to
ReplaceVarsFromTargetList(). The targetlist entries used to replace vars are
no longer offset by that amount. I'm not sure what the original thinking
on it was, but all the existing callers passed sublevels_up = 0, so I hope
this is OK..
---
 src/backend/optimizer/path/allpaths.c            | 149 ++++++++++++++++++-----
 src/backend/optimizer/plan/createplan.c          |  30 +++++
 src/backend/optimizer/prep/prepunion.c           |   4 +-
 src/backend/optimizer/util/pathnode.c            |   6 +-
 src/backend/rewrite/rewriteManip.c               |   6 +-
 src/include/nodes/relation.h                     |   1 +
 src/include/optimizer/pathnode.h                 |   2 +-
 src/test/regress/expected/subselect_pushdown.out |  91 ++++++++++++++
 src/test/regress/parallel_schedule               |   4 +-
 src/test/regress/serial_schedule                 |   3 +-
 src/test/regress/sql/subselect_pushdown.sql      |  54 ++++++++
 11 files changed, 310 insertions(+), 40 deletions(-)
 create mode 100644 src/test/regress/expected/subselect_pushdown.out
 create mode 100644 src/test/regress/sql/subselect_pushdown.sql

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 477b9f7fb8..35537272a3 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -131,12 +131,11 @@ static bool targetIsInAllPartitionLists(TargetEntry *tle, Query *query);
 static bool qual_is_pushdown_safe(Query *subquery, Index rti, Node *qual,
 					  pushdown_safety_info *safetyInfo);
 static void subquery_push_qual(Query *subquery,
-				   RangeTblEntry *rte, Index rti, Node *qual);
+				   RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up);
 static void recurse_push_qual(Node *setOp, Query *topquery,
-				  RangeTblEntry *rte, Index rti, Node *qual);
+				  RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
 
-
 /*
  * make_one_rel
  *	  Finds all possible access paths for executing a query, returning a
@@ -2070,6 +2069,11 @@ has_multiple_baserels(PlannerInfo *root)
  * So the paths made here will be parameterized if the subquery contains
  * LATERAL references, otherwise not.  As long as that's true, there's no need
  * for a separate set_subquery_size phase: just make the paths right away.
+ *
+ * (If a subquery is LATERAL, though, we do push down join clauses that refer
+ * to relations that the subquery already references laterally.  Pushing down
+ * such quals won't make the subquery any more lateral, so there's no reason
+ * not to.)
  */
 static void
 set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
@@ -2082,6 +2086,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	double		tuple_fraction;
 	RelOptInfo *sub_final_rel;
 	ListCell   *lc;
+	List	   *pushed_down_ec_joins = NIL;
 
 	/*
 	 * Must copy the Query so that planning doesn't mess up the RTE contents
@@ -2092,8 +2097,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 
 	/*
 	 * If it's a LATERAL subquery, it might contain some Vars of the current
-	 * query level, requiring it to be treated as parameterized, even though
-	 * we don't support pushing down join quals into subqueries.
+	 * query level, requiring it to be treated as parameterized.
 	 */
 	required_outer = rel->lateral_relids;
 
@@ -2131,38 +2135,112 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	 * pseudoconstant clauses; better to have the gating node above the
 	 * subquery.
 	 *
+	 * Join clauses are only pushed down, if the subquery is LATERAL, and
+	 * the join clause only refers to relations that the subquery already
+	 * depends on.  It might be useful to push down other join clauses, too,
+	 * but then we would need to plan the subquery multiple times, to create
+	 * parameterized paths, which seems too expensive.
+	 *
 	 * Non-pushed-down clauses will get evaluated as qpquals of the
 	 * SubqueryScan node.
 	 *
 	 * XXX Are there any cases where we want to make a policy decision not to
 	 * push down a pushable qual, because it'd result in a worse plan?
 	 */
-	if (rel->baserestrictinfo != NIL &&
+	if ((rel->baserestrictinfo != NIL ||
+		 (!bms_is_empty(required_outer) && (rel->joininfo || rel->has_eclass_joins))) &&
 		subquery_is_pushdown_safe(subquery, subquery, &safetyInfo))
 	{
 		/* OK to consider pushing down individual quals */
-		List	   *upperrestrictlist = NIL;
 		ListCell   *l;
+		Bitmapset *available_relids;
 
-		foreach(l, rel->baserestrictinfo)
+		if (rel->baserestrictinfo)
 		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Node	   *clause = (Node *) rinfo->clause;
+			List	   *upperrestrictlist = NIL;
 
-			if (!rinfo->pseudoconstant &&
-				qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo))
+			foreach(l, rel->baserestrictinfo)
 			{
-				/* Push it down */
-				subquery_push_qual(subquery, rte, rti, clause);
+				RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+				Node	   *clause = (Node *) rinfo->clause;
+
+				if (!rinfo->pseudoconstant &&
+					qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo))
+				{
+					/* Push it down */
+					subquery_push_qual(subquery, rte, rti, clause, 0);
+				}
+				else
+				{
+					/* Keep it in the upper query */
+					upperrestrictlist = lappend(upperrestrictlist, rinfo);
+				}
 			}
-			else
+			rel->baserestrictinfo = upperrestrictlist;
+			/* We don't bother recomputing baserestrict_min_security */
+		}
+
+		/*
+		 * Push down join quals, as well.  But only for LATERAL, and only for those
+		 * relations that are "required" anyway.
+		 */
+		if (!bms_is_empty(required_outer))
+		{
+			available_relids = bms_copy(required_outer);
+			available_relids = bms_add_member(available_relids, rti);
+
+			if (rel->joininfo)
 			{
-				/* Keep it in the upper query */
-				upperrestrictlist = lappend(upperrestrictlist, rinfo);
+				ListCell   *lc;
+				List	   *upperjoinlist = NIL;
+
+				foreach(lc, rel->joininfo)
+				{
+					RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
+					Node       *clause = (Node *) rinfo->clause;
+
+					if (!rinfo->pseudoconstant &&
+						bms_is_subset(rinfo->required_relids, available_relids) &&
+						qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo))
+					{
+						/* Push it down */
+						subquery_push_qual(subquery, rte, rti, clause, 0);
+					}
+					else
+					{
+						/* Keep it in the upper query */
+						upperjoinlist = lappend(upperjoinlist, rinfo);
+					}
+				}
+				rel->joininfo = upperjoinlist;
+			}
+
+			if (rel->has_eclass_joins)
+			{
+				List	   *clauses;
+
+				clauses = generate_join_implied_equalities(root,
+														   available_relids,
+														   required_outer,
+														   rel);
+
+				foreach(lc, clauses)
+				{
+					RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
+					Node       *clause = (Node *) rinfo->clause;
+
+					if (!rinfo->pseudoconstant &&
+						qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo))
+					{
+						/* Push it down */
+						Assert(bms_is_subset(rinfo->required_relids, available_relids));
+						subquery_push_qual(subquery, rte, rti, clause, 0);
+
+						pushed_down_ec_joins = lappend(pushed_down_ec_joins, clause);
+					}
+				}
 			}
 		}
-		rel->baserestrictinfo = upperrestrictlist;
-		/* We don't bother recomputing baserestrict_min_security */
 	}
 
 	pfree(safetyInfo.unsafeColumns);
@@ -2240,7 +2318,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 		/* Generate outer path using this subpath */
 		add_path(rel, (Path *)
 				 create_subqueryscan_path(root, rel, subpath,
-										  pathkeys, required_outer));
+										  pathkeys, required_outer, pushed_down_ec_joins));
 	}
 
 	/* If outer rel allows parallelism, do same for partial paths. */
@@ -2266,7 +2344,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 			add_partial_path(rel, (Path *)
 							 create_subqueryscan_path(root, rel, subpath,
 													  pathkeys,
-													  required_outer));
+													  required_outer, pushed_down_ec_joins));
 		}
 	}
 }
@@ -3174,7 +3252,12 @@ qual_is_pushdown_safe(Query *subquery, Index rti, Node *qual,
 			break;
 		}
 
-		Assert(var->varno == rti);
+		/*
+		 * In a restriction clause, all Vars must refer to subselect output
+		 * columns, but join quals will contain Vars referring to other relations.
+		 */
+		if (var->varno != rti)
+			continue;
 		Assert(var->varattno >= 0);
 
 		/* Check point 4 */
@@ -3201,13 +3284,13 @@ qual_is_pushdown_safe(Query *subquery, Index rti, Node *qual,
  * subquery_push_qual - push down a qual that we have determined is safe
  */
 static void
-subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
+subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up)
 {
 	if (subquery->setOperations != NULL)
 	{
 		/* Recurse to push it separately to each component query */
 		recurse_push_qual(subquery->setOperations, subquery,
-						  rte, rti, qual);
+						  rte, rti, qual, sublevels_up);
 	}
 	else
 	{
@@ -3215,12 +3298,18 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
 		 * We need to replace Vars in the qual (which must refer to outputs of
 		 * the subquery) with copies of the subquery's targetlist expressions.
 		 * Note that at this point, any uplevel Vars in the qual should have
-		 * been replaced with Params, so they need no work.
+		 * been replaced with Params, so they need no work.  But in a join qual,
+		 * there can be Vars referring to other relations at the same level.
+		 * We need to increment varlevelsup of those, so that when the qual is
+		 * pushed down, they refer to the parent query.
 		 *
 		 * This step also ensures that when we are pushing into a setop tree,
 		 * each component query gets its own copy of the qual.
 		 */
-		qual = ReplaceVarsFromTargetList(qual, rti, 0, rte,
+		qual = copyObject(qual);
+		IncrementVarSublevelsUp(qual, sublevels_up + 1, 0);
+
+		qual = ReplaceVarsFromTargetList(qual, rti, sublevels_up + 1, rte,
 										 subquery->targetList,
 										 REPLACEVARS_REPORT_ERROR, 0,
 										 &subquery->hasSubLinks);
@@ -3249,7 +3338,7 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
  */
 static void
 recurse_push_qual(Node *setOp, Query *topquery,
-				  RangeTblEntry *rte, Index rti, Node *qual)
+				  RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up)
 {
 	if (IsA(setOp, RangeTblRef))
 	{
@@ -3258,14 +3347,14 @@ recurse_push_qual(Node *setOp, Query *topquery,
 		Query	   *subquery = subrte->subquery;
 
 		Assert(subquery != NULL);
-		subquery_push_qual(subquery, rte, rti, qual);
+		subquery_push_qual(subquery, rte, rti, qual, sublevels_up + 1);
 	}
 	else if (IsA(setOp, SetOperationStmt))
 	{
 		SetOperationStmt *op = (SetOperationStmt *) setOp;
 
-		recurse_push_qual(op->larg, topquery, rte, rti, qual);
-		recurse_push_qual(op->rarg, topquery, rte, rti, qual);
+		recurse_push_qual(op->larg, topquery, rte, rti, qual, sublevels_up);
+		recurse_push_qual(op->rarg, topquery, rte, rti, qual, sublevels_up);
 	}
 	else
 	{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ca2e0527db..d34c1bf128 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -3186,6 +3186,9 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path,
 	RelOptInfo *rel = best_path->path.parent;
 	Index		scan_relid = rel->relid;
 	Plan	   *subplan;
+	ListCell   *l;
+	List	   *qpqual;
+	List	   *sq_quals = best_path->pushed_down_ec_joins;
 
 	/* it should be a subquery base rel... */
 	Assert(scan_relid > 0);
@@ -3198,6 +3201,33 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path,
 	 */
 	subplan = create_plan(rel->subroot, best_path->subpath);
 
+	/*
+	 * If we had pushed down any join clauses to the subquery, we don't need
+	 * to re-check them in the SubqueryScan node.
+	 *
+	 * This only applies to join clauses derived from equivalence classes.
+	 * Non-join quals, and non-EC-derived join clauses are immediately removed
+	 * from 'baserestrictinfo' and 'joininfo' when they're pushed down, so we
+	 * won't need to worry about them here.
+	 */
+	qpqual = NIL;
+	foreach (l, scan_clauses)
+	{
+		RestrictInfo *rinfo = lfirst_node(RestrictInfo, l);
+
+		if (rinfo->pseudoconstant)
+			continue;			/* we may drop pseudoconstants here */
+		if (list_member_ptr(sq_quals, rinfo))
+			continue;			/* simple duplicate */
+		if (is_redundant_derived_clause(rinfo, sq_quals))
+			continue;			/* derived from same EquivalenceClass */
+		if (!contain_mutable_functions((Node *) rinfo->clause) &&
+			predicate_implied_by(list_make1(rinfo->clause), sq_quals, false))
+			continue;			/* provably implied by indexquals */
+		qpqual = lappend(qpqual, rinfo);
+	}
+	scan_clauses = qpqual;
+
 	/* Sort clauses into best execution order */
 	scan_clauses = order_qual_clauses(root, scan_clauses);
 
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 0ab4014be6..8f424031be 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -321,7 +321,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
 		 * soon too, likely.)
 		 */
 		path = (Path *) create_subqueryscan_path(root, rel, subpath,
-												 NIL, NULL);
+												 NIL, NULL, NIL);
 
 		add_path(rel, path);
 
@@ -339,7 +339,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
 			partial_subpath = linitial(final_rel->partial_pathlist);
 			partial_path = (Path *)
 				create_subqueryscan_path(root, rel, partial_subpath,
-										 NIL, NULL);
+										 NIL, NULL, NIL);
 			add_partial_path(rel, partial_path);
 		}
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index e190ad49d1..e4909c1855 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1869,7 +1869,7 @@ create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
  */
 SubqueryScanPath *
 create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
-						 List *pathkeys, Relids required_outer)
+						 List *pathkeys, Relids required_outer, List *pushed_down_ec_joins)
 {
 	SubqueryScanPath *pathnode = makeNode(SubqueryScanPath);
 
@@ -1884,6 +1884,7 @@ create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	pathnode->path.parallel_workers = subpath->parallel_workers;
 	pathnode->path.pathkeys = pathkeys;
 	pathnode->subpath = subpath;
+	pathnode->pushed_down_ec_joins = pushed_down_ec_joins;
 
 	cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info);
 
@@ -3562,7 +3563,8 @@ reparameterize_path(PlannerInfo *root, Path *path,
 														 rel,
 														 spath->subpath,
 														 spath->path.pathkeys,
-														 required_outer);
+														 required_outer,
+														 spath->pushed_down_ec_joins);
 			}
 		case T_Append:
 			{
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
index f1f4212b5d..5c38e18eb0 100644
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -1400,6 +1400,7 @@ typedef struct
 	List	   *targetlist;
 	ReplaceVarsNoMatchOption nomatch_option;
 	int			nomatch_varno;
+	int			min_sublevels_up;
 } ReplaceVarsFromTargetList_context;
 
 static Node *
@@ -1484,8 +1485,8 @@ ReplaceVarsFromTargetList_callback(Var *var,
 		Expr	   *newnode = copyObject(tle->expr);
 
 		/* Must adjust varlevelsup if tlist item is from higher query */
-		if (var->varlevelsup > 0)
-			IncrementVarSublevelsUp((Node *) newnode, var->varlevelsup, 0);
+		if (var->varlevelsup + rcon->min_sublevels_up > 0)
+			IncrementVarSublevelsUp((Node *) newnode, var->varlevelsup - rcon->min_sublevels_up, 0);
 
 		/*
 		 * Check to see if the tlist item contains a PARAM_MULTIEXPR Param,
@@ -1521,6 +1522,7 @@ ReplaceVarsFromTargetList(Node *node,
 	context.targetlist = targetlist;
 	context.nomatch_option = nomatch_option;
 	context.nomatch_varno = nomatch_varno;
+	context.min_sublevels_up = sublevels_up;
 
 	return replace_rte_variables(node, target_varno, sublevels_up,
 								 ReplaceVarsFromTargetList_callback,
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 3b28d1994f..de942b4e40 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1237,6 +1237,7 @@ typedef struct SubqueryScanPath
 {
 	Path		path;
 	Path	   *subpath;		/* path representing subquery execution */
+	List	   *pushed_down_ec_joins; /* pushed-down quals derived from ECs */
 } SubqueryScanPath;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index e99ae36bef..848839ed15 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -92,7 +92,7 @@ extern GatherMergePath *create_gather_merge_path(PlannerInfo *root,
 						 double *rows);
 extern SubqueryScanPath *create_subqueryscan_path(PlannerInfo *root,
 						 RelOptInfo *rel, Path *subpath,
-						 List *pathkeys, Relids required_outer);
+						 List *pathkeys, Relids required_outer, List *pushed_down_ec_joins);
 extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel,
 						 List *pathkeys, Relids required_outer);
 extern Path *create_tablexprscan_path(PlannerInfo *root, RelOptInfo *rel,
diff --git a/src/test/regress/expected/subselect_pushdown.out b/src/test/regress/expected/subselect_pushdown.out
new file mode 100644
index 0000000000..f160bc84c8
--- /dev/null
+++ b/src/test/regress/expected/subselect_pushdown.out
@@ -0,0 +1,91 @@
+-- Test pushdown of quals into subqueries.
+create table smalltab (i int4, j int4);
+create table bigtab (i int4, j int4);
+insert into smalltab values (1, 1), (100000, 100000);
+insert into bigtab select g,g from generate_series(1, 100000) g;
+analyze smalltab, bigtab;
+create index bigtab_i on bigtab (i);
+-- Push down restriction quals.
+explain (costs off)
+select * from smalltab,
+(
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i = subq.i and smalltab.i = 123;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop
+   ->  Seq Scan on smalltab
+         Filter: (i = 123)
+   ->  GroupAggregate
+         Group Key: bigtab.i
+         ->  Index Scan using bigtab_i on bigtab
+               Index Cond: (i = 123)
+(7 rows)
+
+-- Join quals are not currently pushed down
+explain (costs off)
+select * from smalltab,
+(
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i = subq.i;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Merge Join
+   Merge Cond: (smalltab.i = bigtab.i)
+   ->  Sort
+         Sort Key: smalltab.i
+         ->  Seq Scan on smalltab
+   ->  GroupAggregate
+         Group Key: bigtab.i
+         ->  Index Scan using bigtab_i on bigtab
+(8 rows)
+
+-- Except when the subquery is LATERAL, and already references the other relation.
+-- Such join clauses can be pushed down.
+explain (costs off)
+select * from smalltab,
+lateral (
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  where bigtab.j = smalltab.j
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i < subq.i;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop
+   ->  Seq Scan on smalltab
+   ->  GroupAggregate
+         Group Key: bigtab.i
+         ->  Index Scan using bigtab_i on bigtab
+               Index Cond: (smalltab.i < i)
+               Filter: (j = smalltab.j)
+(7 rows)
+
+-- Multiple join clauses constructed from equivalence classes
+explain (costs off)
+select * from smalltab,
+lateral (
+  select bigtab.i, bigtab.j, avg(bigtab.j)
+  from bigtab
+  where bigtab.j/2 = smalltab.j / 2
+  group by bigtab.i, bigtab.j
+) as subq(i, j, avg)
+where smalltab.i = subq.i and smalltab.j = subq.j;
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
+ Nested Loop
+   ->  Seq Scan on smalltab
+   ->  GroupAggregate
+         Group Key: bigtab.i, bigtab.j
+         ->  Index Scan using bigtab_i on bigtab
+               Index Cond: (smalltab.i = i)
+               Filter: ((smalltab.j = j) AND ((j / 2) = (smalltab.j / 2)))
+(7 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..da3c3b9382 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -79,7 +79,7 @@ ignore: random
 # ----------
 # Another group of parallel tests
 # ----------
-test: select_into select_distinct select_distinct_on select_implicit select_having subselect union case join aggregates transactions random portals arrays btree_index hash_index update namespace prepared_xacts delete
+test: select_into select_distinct select_distinct_on select_implicit select_having union case join aggregates transactions random portals arrays btree_index hash_index update namespace prepared_xacts delete
 
 # ----------
 # Another group of parallel tests
@@ -89,7 +89,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Another group of parallel tests
 # ----------
-test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf tidscan stats_ext
+test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf tidscan stats_ext subselect subselect_pushdown
 
 # rules cannot run concurrently with any test that creates a view
 test: rules psql_crosstab amutils
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..f6ab5f5bac 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -90,7 +90,6 @@ test: select_distinct
 test: select_distinct_on
 test: select_implicit
 test: select_having
-test: subselect
 test: union
 test: case
 test: join
@@ -135,6 +134,8 @@ test: sysviews
 test: tsrf
 test: tidscan
 test: stats_ext
+test: subselect
+test: subselect_pushdown
 test: rules
 test: psql_crosstab
 test: select_parallel
diff --git a/src/test/regress/sql/subselect_pushdown.sql b/src/test/regress/sql/subselect_pushdown.sql
new file mode 100644
index 0000000000..c9e7a43794
--- /dev/null
+++ b/src/test/regress/sql/subselect_pushdown.sql
@@ -0,0 +1,54 @@
+-- Test pushdown of quals into subqueries.
+
+create table smalltab (i int4, j int4);
+create table bigtab (i int4, j int4);
+
+insert into smalltab values (1, 1), (100000, 100000);
+insert into bigtab select g,g from generate_series(1, 100000) g;
+
+analyze smalltab, bigtab;
+
+create index bigtab_i on bigtab (i);
+
+-- Push down restriction quals.
+explain (costs off)
+select * from smalltab,
+(
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i = subq.i and smalltab.i = 123;
+
+-- Join quals are not currently pushed down
+explain (costs off)
+select * from smalltab,
+(
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i = subq.i;
+
+-- Except when the subquery is LATERAL, and already references the other relation.
+-- Such join clauses can be pushed down.
+explain (costs off)
+select * from smalltab,
+lateral (
+  select bigtab.i, avg(bigtab.j)
+  from bigtab
+  where bigtab.j = smalltab.j
+  group by bigtab.i
+) as subq(i, avg)
+where smalltab.i < subq.i;
+
+-- Multiple join clauses constructed from equivalence classes
+explain (costs off)
+select * from smalltab,
+lateral (
+  select bigtab.i, bigtab.j, avg(bigtab.j)
+  from bigtab
+  where bigtab.j/2 = smalltab.j / 2
+  group by bigtab.i, bigtab.j
+) as subq(i, j, avg)
+where smalltab.i = subq.i and smalltab.j = subq.j;
-- 
2.11.0

