diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index f177eba..96d9828 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -2476,6 +2476,8 @@ JumbleRangeTable(pgssJumbleState *jstate, List *rtable)
 			case RTE_NAMEDTUPLESTORE:
 				APP_JUMB_STRING(rte->enrname);
 				break;
+			case RTE_RESULT:
+				break;
 			default:
 				elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind);
 				break;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index bb92d9d..b3894d0 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -5361,7 +5361,7 @@ INSERT INTO ft2 (c1,c2,c3) VALUES (1200,999,'foo') RETURNING tableoid::regclass;
                                                                                            QUERY PLAN                                                                                            
 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Insert on public.ft2
-   Output: (tableoid)::regclass
+   Output: (ft2.tableoid)::regclass
    Remote SQL: INSERT INTO "S 1"."T 1"("C 1", c2, c3, c4, c5, c6, c7, c8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
    ->  Result
          Output: 1200, 999, NULL::integer, 'foo'::text, NULL::timestamp with time zone, NULL::timestamp without time zone, NULL::character varying, 'ft2       '::character(10), NULL::user_enum
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index 12cb18c..cd77f99 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -437,9 +437,12 @@ ExecSupportsMarkRestore(Path *pathnode)
 				return ExecSupportsMarkRestore(((ProjectionPath *) pathnode)->subpath);
 			else if (IsA(pathnode, MinMaxAggPath))
 				return false;	/* childless Result */
+			else if (IsA(pathnode, GroupResultPath))
+				return false;	/* childless Result */
 			else
 			{
-				Assert(IsA(pathnode, ResultPath));
+				/* Simple RTE_RESULT base relation */
+				Assert(IsA(pathnode, Path));
 				return false;	/* childless Result */
 			}
 
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 19b65f6..c3d27a0 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2325,10 +2325,6 @@ range_table_walker(List *rtable,
 				if (walker(rte->tablesample, context))
 					return true;
 				break;
-			case RTE_CTE:
-			case RTE_NAMEDTUPLESTORE:
-				/* nothing to do */
-				break;
 			case RTE_SUBQUERY:
 				if (!(flags & QTW_IGNORE_RT_SUBQUERIES))
 					if (walker(rte->subquery, context))
@@ -2351,6 +2347,11 @@ range_table_walker(List *rtable,
 				if (walker(rte->values_lists, context))
 					return true;
 				break;
+			case RTE_CTE:
+			case RTE_NAMEDTUPLESTORE:
+			case RTE_RESULT:
+				/* nothing to do */
+				break;
 		}
 
 		if (walker(rte->securityQuals, context))
@@ -3156,10 +3157,6 @@ range_table_mutator(List *rtable,
 					   TableSampleClause *);
 				/* we don't bother to copy eref, aliases, etc; OK? */
 				break;
-			case RTE_CTE:
-			case RTE_NAMEDTUPLESTORE:
-				/* nothing to do */
-				break;
 			case RTE_SUBQUERY:
 				if (!(flags & QTW_IGNORE_RT_SUBQUERIES))
 				{
@@ -3190,6 +3187,11 @@ range_table_mutator(List *rtable,
 			case RTE_VALUES:
 				MUTATE(newrte->values_lists, rte->values_lists, List *);
 				break;
+			case RTE_CTE:
+			case RTE_NAMEDTUPLESTORE:
+			case RTE_RESULT:
+				/* nothing to do */
+				break;
 		}
 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
 		newrt = lappend(newrt, newrte);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 0fde876..33f7939 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1855,9 +1855,9 @@ _outMergeAppendPath(StringInfo str, const MergeAppendPath *node)
 }
 
 static void
-_outResultPath(StringInfo str, const ResultPath *node)
+_outGroupResultPath(StringInfo str, const GroupResultPath *node)
 {
-	WRITE_NODE_TYPE("RESULTPATH");
+	WRITE_NODE_TYPE("GROUPRESULTPATH");
 
 	_outPathInfo(str, (const Path *) node);
 
@@ -2213,7 +2213,6 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_ENUM_FIELD(inhTargetKind, InheritanceKind);
 	WRITE_BOOL_FIELD(hasJoinRTEs);
 	WRITE_BOOL_FIELD(hasLateralRTEs);
-	WRITE_BOOL_FIELD(hasDeletedRTEs);
 	WRITE_BOOL_FIELD(hasHavingQual);
 	WRITE_BOOL_FIELD(hasPseudoConstantQuals);
 	WRITE_BOOL_FIELD(hasRecursion);
@@ -3060,6 +3059,9 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 			WRITE_NODE_FIELD(coltypmods);
 			WRITE_NODE_FIELD(colcollations);
 			break;
+		case RTE_RESULT:
+			/* no extra fields */
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d", (int) node->rtekind);
 			break;
@@ -3943,8 +3945,8 @@ outNode(StringInfo str, const void *obj)
 			case T_MergeAppendPath:
 				_outMergeAppendPath(str, obj);
 				break;
-			case T_ResultPath:
-				_outResultPath(str, obj);
+			case T_GroupResultPath:
+				_outGroupResultPath(str, obj);
 				break;
 			case T_MaterialPath:
 				_outMaterialPath(str, obj);
diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c
index 0278108..b9fa3f4 100644
--- a/src/backend/nodes/print.c
+++ b/src/backend/nodes/print.c
@@ -295,6 +295,10 @@ print_rt(const List *rtable)
 				printf("%d\t%s\t[tuplestore]",
 					   i, rte->eref->aliasname);
 				break;
+			case RTE_RESULT:
+				printf("%d\t%s\t[result]",
+					   i, rte->eref->aliasname);
+				break;
 			default:
 				printf("%d\t%s\t[unknown rtekind]",
 					   i, rte->eref->aliasname);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index ec6f256..43491e2 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1411,6 +1411,9 @@ _readRangeTblEntry(void)
 			READ_NODE_FIELD(coltypmods);
 			READ_NODE_FIELD(colcollations);
 			break;
+		case RTE_RESULT:
+			/* no extra fields */
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d",
 				 (int) local_node->rtekind);
diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README
index 9c852a1..89ce373 100644
--- a/src/backend/optimizer/README
+++ b/src/backend/optimizer/README
@@ -361,7 +361,16 @@ RelOptInfo      - a relation or joined relations
                    join clauses)
 
  Path           - every way to generate a RelOptInfo(sequential,index,joins)
-  SeqScan       - represents a sequential scan plan
+  A plain Path node can represent several simple plans, per its pathtype:
+    T_SeqScan   - sequential scan
+    T_SampleScan - tablesample scan
+    T_FunctionScan - function-in-FROM scan
+    T_TableFuncScan - table function scan
+    T_ValuesScan - VALUES scan
+    T_CteScan   - CTE (WITH) scan
+    T_NamedTuplestoreScan - ENR scan
+    T_WorkTableScan - scan worktable of a recursive CTE
+    T_Result    - childless Result plan node (used for FROM-less SELECT)
   IndexPath     - index scan
   BitmapHeapPath - top of a bitmapped index scan
   TidPath       - scan by CTID
@@ -370,7 +379,7 @@ RelOptInfo      - a relation or joined relations
   CustomPath    - for custom scan providers
   AppendPath    - append multiple subpaths together
   MergeAppendPath - merge multiple subpaths, preserving their common sort order
-  ResultPath    - a childless Result plan node (used for FROM-less SELECT)
+  GroupResultPath - childless Result plan node (used for degenerate grouping)
   MaterialPath  - a Material plan node
   UniquePath    - remove duplicate rows (either by hashing or sorting)
   GatherPath    - collect the results of parallel workers
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index bc389b5..f590fc4 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -117,6 +117,8 @@ static void set_cte_pathlist(PlannerInfo *root, RelOptInfo *rel,
 				 RangeTblEntry *rte);
 static void set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel,
 							 RangeTblEntry *rte);
+static void set_result_pathlist(PlannerInfo *root, RelOptInfo *rel,
+					RangeTblEntry *rte);
 static void set_worktable_pathlist(PlannerInfo *root, RelOptInfo *rel,
 					   RangeTblEntry *rte);
 static RelOptInfo *make_rel_from_joinlist(PlannerInfo *root, List *joinlist);
@@ -437,8 +439,13 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 					set_cte_pathlist(root, rel, rte);
 				break;
 			case RTE_NAMEDTUPLESTORE:
+				/* Might as well just build the path immediately */
 				set_namedtuplestore_pathlist(root, rel, rte);
 				break;
+			case RTE_RESULT:
+				/* Might as well just build the path immediately */
+				set_result_pathlist(root, rel, rte);
+				break;
 			default:
 				elog(ERROR, "unexpected rtekind: %d", (int) rel->rtekind);
 				break;
@@ -510,6 +517,9 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 			case RTE_NAMEDTUPLESTORE:
 				/* tuplestore reference --- fully handled during set_rel_size */
 				break;
+			case RTE_RESULT:
+				/* simple Result --- fully handled during set_rel_size */
+				break;
 			default:
 				elog(ERROR, "unexpected rtekind: %d", (int) rel->rtekind);
 				break;
@@ -712,6 +722,10 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 * infrastructure to support that.
 			 */
 			return;
+
+		case RTE_RESULT:
+			/* RESULT RTEs, in themselves, are no problem. */
+			break;
 	}
 
 	/*
@@ -2510,6 +2524,36 @@ set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel,
 }
 
 /*
+ * set_result_pathlist
+ *		Build the (single) access path for an RTE_RESULT RTE
+ *
+ * There's no need for a separate set_result_size phase, since we
+ * don't support join-qual-parameterized paths for these RTEs.
+ */
+static void
+set_result_pathlist(PlannerInfo *root, RelOptInfo *rel,
+					RangeTblEntry *rte)
+{
+	Relids		required_outer;
+
+	/* Mark rel with estimated output rows, width, etc */
+	set_result_size_estimates(root, rel);
+
+	/*
+	 * We don't support pushing join clauses into the quals of a Result scan,
+	 * but it could still have required parameterization due to LATERAL refs
+	 * in its tlist.
+	 */
+	required_outer = rel->lateral_relids;
+
+	/* Generate appropriate path */
+	add_path(rel, create_resultscan_path(root, rel, required_outer));
+
+	/* Select cheapest path (pretty easy in this case...) */
+	set_cheapest(rel);
+}
+
+/*
  * set_worktable_pathlist
  *		Build the (single) access path for a self-reference CTE RTE
  *
@@ -3677,9 +3721,6 @@ print_path(PlannerInfo *root, Path *path, int indent)
 				case T_SampleScan:
 					ptype = "SampleScan";
 					break;
-				case T_SubqueryScan:
-					ptype = "SubqueryScan";
-					break;
 				case T_FunctionScan:
 					ptype = "FunctionScan";
 					break;
@@ -3692,6 +3733,12 @@ print_path(PlannerInfo *root, Path *path, int indent)
 				case T_CteScan:
 					ptype = "CteScan";
 					break;
+				case T_NamedTuplestoreScan:
+					ptype = "NamedTuplestoreScan";
+					break;
+				case T_Result:
+					ptype = "Result";
+					break;
 				case T_WorkTableScan:
 					ptype = "WorkTableScan";
 					break;
@@ -3716,7 +3763,7 @@ print_path(PlannerInfo *root, Path *path, int indent)
 			ptype = "TidScan";
 			break;
 		case T_SubqueryScanPath:
-			ptype = "SubqueryScanScan";
+			ptype = "SubqueryScan";
 			break;
 		case T_ForeignPath:
 			ptype = "ForeignScan";
@@ -3742,8 +3789,8 @@ print_path(PlannerInfo *root, Path *path, int indent)
 		case T_MergeAppendPath:
 			ptype = "MergeAppend";
 			break;
-		case T_ResultPath:
-			ptype = "Result";
+		case T_GroupResultPath:
+			ptype = "GroupResult";
 			break;
 		case T_MaterialPath:
 			ptype = "Material";
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 99c5ad9..30b0e92 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1571,6 +1571,40 @@ cost_namedtuplestorescan(Path *path, PlannerInfo *root,
 }
 
 /*
+ * cost_resultscan
+ *	  Determines and returns the cost of scanning an RTE_RESULT relation.
+ */
+void
+cost_resultscan(Path *path, PlannerInfo *root,
+				RelOptInfo *baserel, ParamPathInfo *param_info)
+{
+	Cost		startup_cost = 0;
+	Cost		run_cost = 0;
+	QualCost	qpqual_cost;
+	Cost		cpu_per_tuple;
+
+	/* Should only be applied to RTE_RESULT base relations */
+	Assert(baserel->relid > 0);
+	Assert(baserel->rtekind == RTE_RESULT);
+
+	/* Mark the path with the correct row estimate */
+	if (param_info)
+		path->rows = param_info->ppi_rows;
+	else
+		path->rows = baserel->rows;
+
+	/* We charge qual cost plus cpu_tuple_cost */
+	get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost);
+
+	startup_cost += qpqual_cost.startup;
+	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
+	run_cost += cpu_per_tuple * baserel->tuples;
+
+	path->startup_cost = startup_cost;
+	path->total_cost = startup_cost + run_cost;
+}
+
+/*
  * cost_recursive_union
  *	  Determines and returns the cost of performing a recursive union,
  *	  and also the estimated output size.
@@ -5045,6 +5079,29 @@ set_namedtuplestore_size_estimates(PlannerInfo *root, RelOptInfo *rel)
 }
 
 /*
+ * set_result_size_estimates
+ *		Set the size estimates for an RTE_RESULT base relation
+ *
+ * The rel's targetlist and restrictinfo list must have been constructed
+ * already.
+ *
+ * We set the same fields as set_baserel_size_estimates.
+ */
+void
+set_result_size_estimates(PlannerInfo *root, RelOptInfo *rel)
+{
+	/* Should only be applied to RTE_RESULT base relations */
+	Assert(rel->relid > 0);
+	Assert(planner_rt_fetch(rel->relid, root)->rtekind == RTE_RESULT);
+
+	/* RTE_RESULT always generates a single row, natively */
+	rel->tuples = 1;
+
+	/* Now estimate number of output rows, etc */
+	set_baserel_size_estimates(root, rel);
+}
+
+/*
  * set_foreign_size_estimates
  *		Set the size estimates for a base relation that is a foreign table.
  *
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 97d0c28..c1aa0ba 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -84,7 +84,8 @@ static Plan *create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 static Plan *create_join_plan(PlannerInfo *root, JoinPath *best_path);
 static Plan *create_append_plan(PlannerInfo *root, AppendPath *best_path);
 static Plan *create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path);
-static Result *create_result_plan(PlannerInfo *root, ResultPath *best_path);
+static Result *create_group_result_plan(PlannerInfo *root,
+						 GroupResultPath *best_path);
 static ProjectSet *create_project_set_plan(PlannerInfo *root, ProjectSetPath *best_path);
 static Material *create_material_plan(PlannerInfo *root, MaterialPath *best_path,
 					 int flags);
@@ -138,6 +139,8 @@ static CteScan *create_ctescan_plan(PlannerInfo *root, Path *best_path,
 					List *tlist, List *scan_clauses);
 static NamedTuplestoreScan *create_namedtuplestorescan_plan(PlannerInfo *root,
 								Path *best_path, List *tlist, List *scan_clauses);
+static Result *create_resultscan_plan(PlannerInfo *root, Path *best_path,
+					   List *tlist, List *scan_clauses);
 static WorkTableScan *create_worktablescan_plan(PlannerInfo *root, Path *best_path,
 						  List *tlist, List *scan_clauses);
 static ForeignScan *create_foreignscan_plan(PlannerInfo *root, ForeignPath *best_path,
@@ -403,11 +406,16 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 				plan = (Plan *) create_minmaxagg_plan(root,
 													  (MinMaxAggPath *) best_path);
 			}
+			else if (IsA(best_path, GroupResultPath))
+			{
+				plan = (Plan *) create_group_result_plan(root,
+														 (GroupResultPath *) best_path);
+			}
 			else
 			{
-				Assert(IsA(best_path, ResultPath));
-				plan = (Plan *) create_result_plan(root,
-												   (ResultPath *) best_path);
+				/* Simple RTE_RESULT base relation */
+				Assert(IsA(best_path, Path));
+				plan = create_scan_plan(root, best_path, flags);
 			}
 			break;
 		case T_ProjectSet:
@@ -691,6 +699,13 @@ create_scan_plan(PlannerInfo *root, Path *best_path, int flags)
 															scan_clauses);
 			break;
 
+		case T_Result:
+			plan = (Plan *) create_resultscan_plan(root,
+												   best_path,
+												   tlist,
+												   scan_clauses);
+			break;
+
 		case T_WorkTableScan:
 			plan = (Plan *) create_worktablescan_plan(root,
 													  best_path,
@@ -922,17 +937,34 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 				   List *gating_quals)
 {
 	Plan	   *gplan;
+	Plan	   *splan;
 
 	Assert(gating_quals);
 
 	/*
+	 * We might have a trivial Result plan already.  Stacking one Result atop
+	 * another is silly, so if that applies, just discard the input plan.
+	 * (We're assuming its targetlist is uninteresting; it should be either
+	 * the same as the result of build_path_tlist, or a simplified version.)
+	 */
+	splan = plan;
+	if (IsA(plan, Result))
+	{
+		Result	   *rplan = (Result *) plan;
+
+		if (rplan->plan.lefttree == NULL &&
+			rplan->resconstantqual == NULL)
+			splan = NULL;
+	}
+
+	/*
 	 * Since we need a Result node anyway, always return the path's requested
 	 * tlist; that's never a wrong choice, even if the parent node didn't ask
 	 * for CP_EXACT_TLIST.
 	 */
 	gplan = (Plan *) make_result(build_path_tlist(root, path),
 								 (Node *) gating_quals,
-								 plan);
+								 splan);
 
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
@@ -1254,15 +1286,14 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path)
 }
 
 /*
- * create_result_plan
+ * create_group_result_plan
  *	  Create a Result plan for 'best_path'.
- *	  This is only used for degenerate cases, such as a query with an empty
- *	  jointree.
+ *	  This is only used for degenerate grouping cases.
  *
  *	  Returns a Plan node.
  */
 static Result *
-create_result_plan(PlannerInfo *root, ResultPath *best_path)
+create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 {
 	Result	   *plan;
 	List	   *tlist;
@@ -3478,6 +3509,44 @@ create_namedtuplestorescan_plan(PlannerInfo *root, Path *best_path,
 }
 
 /*
+ * create_resultscan_plan
+ *	 Returns a Result plan for the RTE_RESULT base relation scanned by
+ *	'best_path' with restriction clauses 'scan_clauses' and targetlist
+ *	'tlist'.
+ */
+static Result *
+create_resultscan_plan(PlannerInfo *root, Path *best_path,
+					   List *tlist, List *scan_clauses)
+{
+	Result	   *scan_plan;
+	Index		scan_relid = best_path->parent->relid;
+	RangeTblEntry *rte PG_USED_FOR_ASSERTS_ONLY;
+
+	Assert(scan_relid > 0);
+	rte = planner_rt_fetch(scan_relid, root);
+	Assert(rte->rtekind == RTE_RESULT);
+
+	/* Sort clauses into best execution order */
+	scan_clauses = order_qual_clauses(root, scan_clauses);
+
+	/* Reduce RestrictInfo list to bare expressions; ignore pseudoconstants */
+	scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+	/* Replace any outer-relation variables with nestloop params */
+	if (best_path->param_info)
+	{
+		scan_clauses = (List *)
+			replace_nestloop_params(root, (Node *) scan_clauses);
+	}
+
+	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+
+	copy_generic_path_info(&scan_plan->plan, best_path);
+
+	return scan_plan;
+}
+
+/*
  * create_worktablescan_plan
  *	 Returns a worktablescan plan for the base relation scanned by 'best_path'
  *	 with restriction clauses 'scan_clauses' and targetlist 'tlist'.
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index a663740..1c78852 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -827,7 +827,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 		 * all below it, so we should report inner_join_rels = qualscope. If
 		 * there was exactly one element, we should (and already did) report
 		 * whatever its inner_join_rels were.  If there were no elements (is
-		 * that possible?) the initialization before the loop fixed it.
+		 * that still possible?) the initialization before the loop fixed it.
 		 */
 		if (list_length(f->fromlist) > 1)
 			*inner_join_rels = *qualscope;
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index fc97a1b..f9f4b12 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -61,44 +61,6 @@ query_planner(PlannerInfo *root, List *tlist,
 	RelOptInfo *final_rel;
 
 	/*
-	 * If the query has an empty join tree, then it's something easy like
-	 * "SELECT 2+2;" or "INSERT ... VALUES()".  Fall through quickly.
-	 */
-	if (parse->jointree->fromlist == NIL)
-	{
-		/* We need a dummy joinrel to describe the empty set of baserels */
-		final_rel = build_empty_join_rel(root);
-
-		/*
-		 * If query allows parallelism in general, check whether the quals are
-		 * parallel-restricted.  (We need not check final_rel->reltarget
-		 * because it's empty at this point.  Anything parallel-restricted in
-		 * the query tlist will be dealt with later.)
-		 */
-		if (root->glob->parallelModeOK)
-			final_rel->consider_parallel =
-				is_parallel_safe(root, parse->jointree->quals);
-
-		/* The only path for it is a trivial Result path */
-		add_path(final_rel, (Path *)
-				 create_result_path(root, final_rel,
-									final_rel->reltarget,
-									(List *) parse->jointree->quals));
-
-		/* Select cheapest path (pretty easy in this case...) */
-		set_cheapest(final_rel);
-
-		/*
-		 * We still are required to call qp_callback, in case it's something
-		 * like "SELECT 2+2 ORDER BY 1".
-		 */
-		root->canon_pathkeys = NIL;
-		(*qp_callback) (root, qp_extra);
-
-		return final_rel;
-	}
-
-	/*
 	 * Init planner lists to empty.
 	 *
 	 * NOTE: append_rel_list was set up by subquery_planner, so do not touch
@@ -125,6 +87,71 @@ query_planner(PlannerInfo *root, List *tlist,
 	setup_simple_rel_arrays(root);
 
 	/*
+	 * In the trivial case where the jointree is a single RTE_RESULT relation,
+	 * bypass all the rest of this function and just make a RelOptInfo and its
+	 * one access path.  This is worth optimizing because it applies for
+	 * common cases like "SELECT expression" and "INSERT ... VALUES()".
+	 */
+	Assert(parse->jointree->fromlist != NIL);
+	if (list_length(parse->jointree->fromlist) == 1)
+	{
+		Node	   *jtnode = (Node *) linitial(parse->jointree->fromlist);
+
+		if (IsA(jtnode, RangeTblRef))
+		{
+			int			varno = ((RangeTblRef *) jtnode)->rtindex;
+			RangeTblEntry *rte = root->simple_rte_array[varno];
+
+			Assert(rte != NULL);
+			if (rte->rtekind == RTE_RESULT)
+			{
+				/* Make the RelOptInfo for it directly */
+				final_rel = build_simple_rel(root, varno, NULL);
+
+				/*
+				 * If query allows parallelism in general, check whether the
+				 * quals are parallel-restricted.  (We need not check
+				 * final_rel->reltarget because it's empty at this point.
+				 * Anything parallel-restricted in the query tlist will be
+				 * dealt with later.)  This is normally pretty silly, because
+				 * a Result-only plan would never be interesting to
+				 * parallelize.  However, if force_parallel_mode is on, then
+				 * we want to execute the Result in a parallel worker if
+				 * possible, so we must do this.
+				 */
+				if (root->glob->parallelModeOK &&
+					force_parallel_mode != FORCE_PARALLEL_OFF)
+					final_rel->consider_parallel =
+						is_parallel_safe(root, parse->jointree->quals);
+
+				/*
+				 * The only path for it is a trivial Result path.  We cheat a
+				 * bit here by using a GroupResultPath, because that way we
+				 * can just jam the quals into it without preprocessing them.
+				 * (But, if you hold your head at the right angle, a FROM-less
+				 * SELECT is a kind of degenerate-grouping case, so it's not
+				 * that much of a cheat.)
+				 */
+				add_path(final_rel, (Path *)
+						 create_group_result_path(root, final_rel,
+												  final_rel->reltarget,
+												  (List *) parse->jointree->quals));
+
+				/* Select cheapest path (pretty easy in this case...) */
+				set_cheapest(final_rel);
+
+				/*
+				 * We still are required to call qp_callback, in case it's
+				 * something like "SELECT 2+2 ORDER BY 1".
+				 */
+				(*qp_callback) (root, qp_extra);
+
+				return final_rel;
+			}
+		}
+	}
+
+	/*
 	 * Populate append_rel_array with each AppendRelInfo to allow direct
 	 * lookups by child relid.
 	 */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b849ae0..709935a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -609,6 +609,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	List	   *newWithCheckOptions;
 	List	   *newHaving;
 	bool		hasOuterJoins;
+	bool		hasResultRTEs;
 	RelOptInfo *final_rel;
 	ListCell   *l;
 
@@ -650,6 +651,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		SS_process_ctes(root);
 
 	/*
+	 * If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so
+	 * that we don't need so many special cases to deal with that situation.
+	 */
+	replace_empty_jointree(parse);
+
+	/*
 	 * Look for ANY and EXISTS SubLinks in WHERE and JOIN/ON clauses, and try
 	 * to transform them into joins.  Note that this step does not descend
 	 * into subqueries; if we pull up any subqueries below, their SubLinks are
@@ -682,14 +689,16 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 
 	/*
 	 * Detect whether any rangetable entries are RTE_JOIN kind; if not, we can
-	 * avoid the expense of doing flatten_join_alias_vars().  Also check for
-	 * outer joins --- if none, we can skip reduce_outer_joins().  And check
-	 * for LATERAL RTEs, too.  This must be done after we have done
-	 * pull_up_subqueries(), of course.
+	 * avoid the expense of doing flatten_join_alias_vars().  Likewise check
+	 * whether any are RTE_RESULT kind; if not, we can skip
+	 * remove_useless_result_rtes().  Also check for outer joins --- if none,
+	 * we can skip reduce_outer_joins().  And check for LATERAL RTEs, too.
+	 * This must be done after we have done pull_up_subqueries(), of course.
 	 */
 	root->hasJoinRTEs = false;
 	root->hasLateralRTEs = false;
 	hasOuterJoins = false;
+	hasResultRTEs = false;
 	foreach(l, parse->rtable)
 	{
 		RangeTblEntry *rte = lfirst_node(RangeTblEntry, l);
@@ -700,6 +709,10 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 			if (IS_OUTER_JOIN(rte->jointype))
 				hasOuterJoins = true;
 		}
+		else if (rte->rtekind == RTE_RESULT)
+		{
+			hasResultRTEs = true;
+		}
 		if (rte->lateral)
 			root->hasLateralRTEs = true;
 	}
@@ -715,10 +728,10 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	/*
 	 * Expand any rangetable entries that are inheritance sets into "append
 	 * relations".  This can add entries to the rangetable, but they must be
-	 * plain base relations not joins, so it's OK (and marginally more
-	 * efficient) to do it after checking for join RTEs.  We must do it after
-	 * pulling up subqueries, else we'd fail to handle inherited tables in
-	 * subqueries.
+	 * plain RTE_RELATION entries, so it's OK (and marginally more efficient)
+	 * to do it after checking for joins and other special RTEs.  We must do
+	 * this after pulling up subqueries, else we'd fail to handle inherited
+	 * tables in subqueries.
 	 */
 	expand_inherited_tables(root);
 
@@ -966,6 +979,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		reduce_outer_joins(root);
 
 	/*
+	 * If we have any RTE_RESULT relations, see if they can be deleted from
+	 * the jointree.  This step is most effectively done after we've done
+	 * expression preprocessing and outer join reduction.
+	 */
+	if (hasResultRTEs)
+		remove_useless_result_rtes(root);
+
+	/*
 	 * Do the main planning.  If we have an inherited target relation, that
 	 * needs special processing, else go straight to grouping_planner.
 	 */
@@ -3892,9 +3913,9 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 		while (--nrows >= 0)
 		{
 			path = (Path *)
-				create_result_path(root, grouped_rel,
-								   grouped_rel->reltarget,
-								   (List *) parse->havingQual);
+				create_group_result_path(root, grouped_rel,
+										 grouped_rel->reltarget,
+										 (List *) parse->havingQual);
 			paths = lappend(paths, path);
 		}
 		path = (Path *)
@@ -3912,9 +3933,9 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 	{
 		/* No grouping sets, or just one, so one output row */
 		path = (Path *)
-			create_result_path(root, grouped_rel,
-							   grouped_rel->reltarget,
-							   (List *) parse->havingQual);
+			create_group_result_path(root, grouped_rel,
+									 grouped_rel->reltarget,
+									 (List *) parse->havingQual);
 	}
 
 	add_path(grouped_rel, path);
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 64272dd..fd19d0a 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1115,12 +1115,6 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink,
 		return NULL;
 
 	/*
-	 * The subquery must have a nonempty jointree, else we won't have a join.
-	 */
-	if (subselect->jointree->fromlist == NIL)
-		return NULL;
-
-	/*
 	 * Separate out the WHERE clause.  (We could theoretically also remove
 	 * top-level plain JOIN/ON clauses, but it's probably not worth the
 	 * trouble.)
@@ -1149,6 +1143,11 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink,
 		return NULL;
 
 	/*
+	 * The subquery must have a nonempty jointree, but we can make it so.
+	 */
+	replace_empty_jointree(subselect);
+
+	/*
 	 * Prepare to pull up the sub-select into top range table.
 	 *
 	 * We rely here on the assumption that the outer query has no references
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 77dbf4e..bcbca1a 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -4,12 +4,14 @@
  *	  Planner preprocessing for subqueries and join tree manipulation.
  *
  * NOTE: the intended sequence for invoking these operations is
+ *		replace_empty_jointree
  *		pull_up_sublinks
  *		inline_set_returning_functions
  *		pull_up_subqueries
  *		flatten_simple_union_all
  *		do expression preprocessing (including flattening JOIN alias vars)
  *		reduce_outer_joins
+ *		remove_useless_result_rtes
  *
  *
  * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
@@ -66,14 +68,12 @@ static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 static Node *pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 						   JoinExpr *lowest_outer_join,
 						   JoinExpr *lowest_nulling_outer_join,
-						   AppendRelInfo *containing_appendrel,
-						   bool deletion_ok);
+						   AppendRelInfo *containing_appendrel);
 static Node *pull_up_simple_subquery(PlannerInfo *root, Node *jtnode,
 						RangeTblEntry *rte,
 						JoinExpr *lowest_outer_join,
 						JoinExpr *lowest_nulling_outer_join,
-						AppendRelInfo *containing_appendrel,
-						bool deletion_ok);
+						AppendRelInfo *containing_appendrel);
 static Node *pull_up_simple_union_all(PlannerInfo *root, Node *jtnode,
 						 RangeTblEntry *rte);
 static void pull_up_union_leaf_queries(Node *setOp, PlannerInfo *root,
@@ -82,12 +82,10 @@ static void pull_up_union_leaf_queries(Node *setOp, PlannerInfo *root,
 static void make_setop_translation_list(Query *query, Index newvarno,
 							List **translated_vars);
 static bool is_simple_subquery(Query *subquery, RangeTblEntry *rte,
-				   JoinExpr *lowest_outer_join,
-				   bool deletion_ok);
+				   JoinExpr *lowest_outer_join);
 static Node *pull_up_simple_values(PlannerInfo *root, Node *jtnode,
 					  RangeTblEntry *rte);
-static bool is_simple_values(PlannerInfo *root, RangeTblEntry *rte,
-				 bool deletion_ok);
+static bool is_simple_values(PlannerInfo *root, RangeTblEntry *rte);
 static bool is_simple_union_all(Query *subquery);
 static bool is_simple_union_all_recurse(Node *setOp, Query *setOpQuery,
 							List *colTypes);
@@ -103,7 +101,6 @@ static Node *pullup_replace_vars_callback(Var *var,
 							 replace_rte_variables_context *context);
 static Query *pullup_replace_vars_subquery(Query *query,
 							 pullup_replace_vars_context *context);
-static Node *pull_up_subqueries_cleanup(Node *jtnode);
 static reduce_outer_joins_state *reduce_outer_joins_pass1(Node *jtnode);
 static void reduce_outer_joins_pass2(Node *jtnode,
 						 reduce_outer_joins_state *state,
@@ -111,14 +108,62 @@ static void reduce_outer_joins_pass2(Node *jtnode,
 						 Relids nonnullable_rels,
 						 List *nonnullable_vars,
 						 List *forced_null_vars);
-static void substitute_multiple_relids(Node *node,
-						   int varno, Relids subrelids);
+static Node *remove_useless_results_recurse(PlannerInfo *root, Node *jtnode);
+static int	get_result_relid(PlannerInfo *root, Node *jtnode);
+static void remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc);
+static bool find_dependent_phvs(Node *node, int varno);
+static void substitute_phv_relids(Node *node,
+					  int varno, Relids subrelids);
 static void fix_append_rel_relids(List *append_rel_list, int varno,
 					  Relids subrelids);
 static Node *find_jointree_node_for_rel(Node *jtnode, int relid);
 
 
 /*
+ * replace_empty_jointree
+ *		If the Query's jointree is empty, replace it with a dummy RTE_RESULT
+ *		relation.
+ *
+ * By doing this, we can avoid a bunch of corner cases that formerly existed
+ * for SELECTs with omitted FROM clauses.  An example is that a subquery
+ * with empty jointree previously could not be pulled up, because that would
+ * have resulted in an empty relid set, making the subquery not uniquely
+ * identifiable for join or PlaceHolderVar processing.
+ *
+ * Unlike most other functions in this file, this function doesn't recurse;
+ * we rely on other processing to invoke it on sub-queries at suitable times.
+ */
+void
+replace_empty_jointree(Query *parse)
+{
+	RangeTblEntry *rte;
+	Index		rti;
+	RangeTblRef *rtr;
+
+	/* Nothing to do if jointree is already nonempty */
+	if (parse->jointree->fromlist != NIL)
+		return;
+
+	/* We mustn't change it in the top level of a setop tree, either */
+	if (parse->setOperations)
+		return;
+
+	/* Create suitable RTE */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RESULT;
+	rte->eref = makeAlias("*RESULT*", NIL);
+
+	/* Add it to rangetable */
+	parse->rtable = lappend(parse->rtable, rte);
+	rti = list_length(parse->rtable);
+
+	/* And jam a reference into the jointree */
+	rtr = makeNode(RangeTblRef);
+	rtr->rtindex = rti;
+	parse->jointree->fromlist = list_make1(rtr);
+}
+
+/*
  * pull_up_sublinks
  *		Attempt to pull up ANY and EXISTS SubLinks to be treated as
  *		semijoins or anti-semijoins.
@@ -611,16 +656,11 @@ pull_up_subqueries(PlannerInfo *root)
 {
 	/* Top level of jointree must always be a FromExpr */
 	Assert(IsA(root->parse->jointree, FromExpr));
-	/* Reset flag saying we need a deletion cleanup pass */
-	root->hasDeletedRTEs = false;
 	/* Recursion starts with no containing join nor appendrel */
 	root->parse->jointree = (FromExpr *)
 		pull_up_subqueries_recurse(root, (Node *) root->parse->jointree,
-								   NULL, NULL, NULL, false);
-	/* Apply cleanup phase if necessary */
-	if (root->hasDeletedRTEs)
-		root->parse->jointree = (FromExpr *)
-			pull_up_subqueries_cleanup((Node *) root->parse->jointree);
+								   NULL, NULL, NULL);
+	/* We should still have a FromExpr */
 	Assert(IsA(root->parse->jointree, FromExpr));
 }
 
@@ -629,8 +669,6 @@ pull_up_subqueries(PlannerInfo *root)
  *		Recursive guts of pull_up_subqueries.
  *
  * This recursively processes the jointree and returns a modified jointree.
- * Or, if it's valid to drop the current node from the jointree completely,
- * it returns NULL.
  *
  * If this jointree node is within either side of an outer join, then
  * lowest_outer_join references the lowest such JoinExpr node; otherwise
@@ -647,37 +685,27 @@ pull_up_subqueries(PlannerInfo *root)
  * This forces use of the PlaceHolderVar mechanism for all non-Var targetlist
  * items, and puts some additional restrictions on what can be pulled up.
  *
- * deletion_ok is true if the caller can cope with us returning NULL for a
- * deletable leaf node (for example, a VALUES RTE that could be pulled up).
- * If it's false, we'll avoid pullup in such cases.
- *
  * A tricky aspect of this code is that if we pull up a subquery we have
  * to replace Vars that reference the subquery's outputs throughout the
  * parent query, including quals attached to jointree nodes above the one
- * we are currently processing!  We handle this by being careful not to
- * change the jointree structure while recursing: no nodes other than leaf
- * RangeTblRef entries and entirely-empty FromExprs will be replaced or
- * deleted.  Also, we can't turn pullup_replace_vars loose on the whole
- * jointree, because it'll return a mutated copy of the tree; we have to
+ * we are currently processing!  We handle this by being careful to maintain
+ * validity of the jointree structure while recursing, in the following sense:
+ * whenever we recurse, all qual expressions in the tree must be reachable
+ * from the top level, in case the recursive call needs to modify them.
+ *
+ * Notice also that we can't turn pullup_replace_vars loose on the whole
+ * jointree, because it'd return a mutated copy of the tree; we have to
  * invoke it just on the quals, instead.  This behavior is what makes it
  * reasonable to pass lowest_outer_join and lowest_nulling_outer_join as
  * pointers rather than some more-indirect way of identifying the lowest
  * OJs.  Likewise, we don't replace append_rel_list members but only their
  * substructure, so the containing_appendrel reference is safe to use.
- *
- * Because of the rule that no jointree nodes with substructure can be
- * replaced, we cannot fully handle the case of deleting nodes from the tree:
- * when we delete one child of a JoinExpr, we need to replace the JoinExpr
- * with a FromExpr, and that can't happen here.  Instead, we set the
- * root->hasDeletedRTEs flag, which tells pull_up_subqueries() that an
- * additional pass over the tree is needed to clean up.
  */
 static Node *
 pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 						   JoinExpr *lowest_outer_join,
 						   JoinExpr *lowest_nulling_outer_join,
-						   AppendRelInfo *containing_appendrel,
-						   bool deletion_ok)
+						   AppendRelInfo *containing_appendrel)
 {
 	Assert(jtnode != NULL);
 	if (IsA(jtnode, RangeTblRef))
@@ -693,15 +721,13 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		 * unless is_safe_append_member says so.
 		 */
 		if (rte->rtekind == RTE_SUBQUERY &&
-			is_simple_subquery(rte->subquery, rte,
-							   lowest_outer_join, deletion_ok) &&
+			is_simple_subquery(rte->subquery, rte, lowest_outer_join) &&
 			(containing_appendrel == NULL ||
 			 is_safe_append_member(rte->subquery)))
 			return pull_up_simple_subquery(root, jtnode, rte,
 										   lowest_outer_join,
 										   lowest_nulling_outer_join,
-										   containing_appendrel,
-										   deletion_ok);
+										   containing_appendrel);
 
 		/*
 		 * Alternatively, is it a simple UNION ALL subquery?  If so, flatten
@@ -725,7 +751,7 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		if (rte->rtekind == RTE_VALUES &&
 			lowest_outer_join == NULL &&
 			containing_appendrel == NULL &&
-			is_simple_values(root, rte, deletion_ok))
+			is_simple_values(root, rte))
 			return pull_up_simple_values(root, jtnode, rte);
 
 		/* Otherwise, do nothing at this node. */
@@ -733,50 +759,16 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 	else if (IsA(jtnode, FromExpr))
 	{
 		FromExpr   *f = (FromExpr *) jtnode;
-		bool		have_undeleted_child = false;
 		ListCell   *l;
 
 		Assert(containing_appendrel == NULL);
-
-		/*
-		 * If the FromExpr has quals, it's not deletable even if its parent
-		 * would allow deletion.
-		 */
-		if (f->quals)
-			deletion_ok = false;
-
+		/* Recursively transform all the child nodes */
 		foreach(l, f->fromlist)
 		{
-			/*
-			 * In a non-deletable FromExpr, we can allow deletion of child
-			 * nodes so long as at least one child remains; so it's okay
-			 * either if any previous child survives, or if there's more to
-			 * come.  If all children are deletable in themselves, we'll force
-			 * the last one to remain unflattened.
-			 *
-			 * As a separate matter, we can allow deletion of all children of
-			 * the top-level FromExpr in a query, since that's a special case
-			 * anyway.
-			 */
-			bool		sub_deletion_ok = (deletion_ok ||
-										   have_undeleted_child ||
-										   lnext(l) != NULL ||
-										   f == root->parse->jointree);
-
 			lfirst(l) = pull_up_subqueries_recurse(root, lfirst(l),
 												   lowest_outer_join,
 												   lowest_nulling_outer_join,
-												   NULL,
-												   sub_deletion_ok);
-			if (lfirst(l) != NULL)
-				have_undeleted_child = true;
-		}
-
-		if (deletion_ok && !have_undeleted_child)
-		{
-			/* OK to delete this FromExpr entirely */
-			root->hasDeletedRTEs = true;	/* probably is set already */
-			return NULL;
+												   NULL);
 		}
 	}
 	else if (IsA(jtnode, JoinExpr))
@@ -788,22 +780,14 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
-
-				/*
-				 * INNER JOIN can allow deletion of either child node, but not
-				 * both.  So right child gets permission to delete only if
-				 * left child didn't get removed.
-				 */
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 lowest_outer_join,
 													 lowest_nulling_outer_join,
-													 NULL,
-													 true);
+													 NULL);
 				j->rarg = pull_up_subqueries_recurse(root, j->rarg,
 													 lowest_outer_join,
 													 lowest_nulling_outer_join,
-													 NULL,
-													 j->larg != NULL);
+													 NULL);
 				break;
 			case JOIN_LEFT:
 			case JOIN_SEMI:
@@ -811,37 +795,31 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 j,
 													 lowest_nulling_outer_join,
-													 NULL,
-													 false);
+													 NULL);
 				j->rarg = pull_up_subqueries_recurse(root, j->rarg,
 													 j,
 													 j,
-													 NULL,
-													 false);
+													 NULL);
 				break;
 			case JOIN_FULL:
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 j,
 													 j,
-													 NULL,
-													 false);
+													 NULL);
 				j->rarg = pull_up_subqueries_recurse(root, j->rarg,
 													 j,
 													 j,
-													 NULL,
-													 false);
+													 NULL);
 				break;
 			case JOIN_RIGHT:
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 j,
 													 j,
-													 NULL,
-													 false);
+													 NULL);
 				j->rarg = pull_up_subqueries_recurse(root, j->rarg,
 													 j,
 													 lowest_nulling_outer_join,
-													 NULL,
-													 false);
+													 NULL);
 				break;
 			default:
 				elog(ERROR, "unrecognized join type: %d",
@@ -861,8 +839,8 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
  *
  * jtnode is a RangeTblRef that has been tentatively identified as a simple
  * subquery by pull_up_subqueries.  We return the replacement jointree node,
- * or NULL if the subquery can be deleted entirely, or jtnode itself if we
- * determine that the subquery can't be pulled up after all.
+ * or jtnode itself if we determine that the subquery can't be pulled up
+ * after all.
  *
  * rte is the RangeTblEntry referenced by jtnode.  Remaining parameters are
  * as for pull_up_subqueries_recurse.
@@ -871,8 +849,7 @@ static Node *
 pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 						JoinExpr *lowest_outer_join,
 						JoinExpr *lowest_nulling_outer_join,
-						AppendRelInfo *containing_appendrel,
-						bool deletion_ok)
+						AppendRelInfo *containing_appendrel)
 {
 	Query	   *parse = root->parse;
 	int			varno = ((RangeTblRef *) jtnode)->rtindex;
@@ -926,6 +903,12 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	Assert(subquery->cteList == NIL);
 
 	/*
+	 * If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so
+	 * that we don't need so many special cases to deal with that situation.
+	 */
+	replace_empty_jointree(subquery);
+
+	/*
 	 * Pull up any SubLinks within the subquery's quals, so that we don't
 	 * leave unoptimized SubLinks behind.
 	 */
@@ -957,8 +940,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	 * easier just to keep this "if" looking the same as the one in
 	 * pull_up_subqueries_recurse.
 	 */
-	if (is_simple_subquery(subquery, rte,
-						   lowest_outer_join, deletion_ok) &&
+	if (is_simple_subquery(subquery, rte, lowest_outer_join) &&
 		(containing_appendrel == NULL || is_safe_append_member(subquery)))
 	{
 		/* good to go */
@@ -1159,6 +1141,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 				case RTE_JOIN:
 				case RTE_CTE:
 				case RTE_NAMEDTUPLESTORE:
+				case RTE_RESULT:
 					/* these can't contain any lateral references */
 					break;
 			}
@@ -1195,7 +1178,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 		Relids		subrelids;
 
 		subrelids = get_relids_in_jointree((Node *) subquery->jointree, false);
-		substitute_multiple_relids((Node *) parse, varno, subrelids);
+		substitute_phv_relids((Node *) parse, varno, subrelids);
 		fix_append_rel_relids(root->append_rel_list, varno, subrelids);
 	}
 
@@ -1235,17 +1218,14 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 
 	/*
 	 * Return the adjusted subquery jointree to replace the RangeTblRef entry
-	 * in parent's jointree; or, if we're flattening a subquery with empty
-	 * FROM list, return NULL to signal deletion of the subquery from the
-	 * parent jointree (and set hasDeletedRTEs to ensure cleanup later).
+	 * in parent's jointree; or, if the FromExpr is degenerate, just return
+	 * its single member.
 	 */
-	if (subquery->jointree->fromlist == NIL)
-	{
-		Assert(deletion_ok);
-		Assert(subquery->jointree->quals == NULL);
-		root->hasDeletedRTEs = true;
-		return NULL;
-	}
+	Assert(IsA(subquery->jointree, FromExpr));
+	Assert(subquery->jointree->fromlist != NIL);
+	if (subquery->jointree->quals == NULL &&
+		list_length(subquery->jointree->fromlist) == 1)
+		return (Node *) linitial(subquery->jointree->fromlist);
 
 	return (Node *) subquery->jointree;
 }
@@ -1381,7 +1361,7 @@ pull_up_union_leaf_queries(Node *setOp, PlannerInfo *root, int parentRTindex,
 		rtr = makeNode(RangeTblRef);
 		rtr->rtindex = childRTindex;
 		(void) pull_up_subqueries_recurse(root, (Node *) rtr,
-										  NULL, NULL, appinfo, false);
+										  NULL, NULL, appinfo);
 	}
 	else if (IsA(setOp, SetOperationStmt))
 	{
@@ -1436,12 +1416,10 @@ make_setop_translation_list(Query *query, Index newvarno,
  * (Note subquery is not necessarily equal to rte->subquery; it could be a
  * processed copy of that.)
  * lowest_outer_join is the lowest outer join above the subquery, or NULL.
- * deletion_ok is true if it'd be okay to delete the subquery entirely.
  */
 static bool
 is_simple_subquery(Query *subquery, RangeTblEntry *rte,
-				   JoinExpr *lowest_outer_join,
-				   bool deletion_ok)
+				   JoinExpr *lowest_outer_join)
 {
 	/*
 	 * Let's just make sure it's a valid subselect ...
@@ -1491,44 +1469,6 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte,
 		return false;
 
 	/*
-	 * Don't pull up a subquery with an empty jointree, unless it has no quals
-	 * and deletion_ok is true and we're not underneath an outer join.
-	 *
-	 * query_planner() will correctly generate a Result plan for a jointree
-	 * that's totally empty, but we can't cope with an empty FromExpr
-	 * appearing lower down in a jointree: we identify join rels via baserelid
-	 * sets, so we couldn't distinguish a join containing such a FromExpr from
-	 * one without it.  We can only handle such cases if the place where the
-	 * subquery is linked is a FromExpr or inner JOIN that would still be
-	 * nonempty after removal of the subquery, so that it's still identifiable
-	 * via its contained baserelids.  Safe contexts are signaled by
-	 * deletion_ok.
-	 *
-	 * But even in a safe context, we must keep the subquery if it has any
-	 * quals, because it's unclear where to put them in the upper query.
-	 *
-	 * Also, we must forbid pullup if such a subquery is underneath an outer
-	 * join, because then we might need to wrap its output columns with
-	 * PlaceHolderVars, and the PHVs would then have empty relid sets meaning
-	 * we couldn't tell where to evaluate them.  (This test is separate from
-	 * the deletion_ok flag for possible future expansion: deletion_ok tells
-	 * whether the immediate parent site in the jointree could cope, not
-	 * whether we'd have PHV issues.  It's possible this restriction could be
-	 * fixed by letting the PHVs use the relids of the parent jointree item,
-	 * but that complication is for another day.)
-	 *
-	 * Note that deletion of a subquery is also dependent on the check below
-	 * that its targetlist contains no set-returning functions.  Deletion from
-	 * a FROM list or inner JOIN is okay only if the subquery must return
-	 * exactly one row.
-	 */
-	if (subquery->jointree->fromlist == NIL &&
-		(subquery->jointree->quals != NULL ||
-		 !deletion_ok ||
-		 lowest_outer_join != NULL))
-		return false;
-
-	/*
 	 * If the subquery is LATERAL, check for pullup restrictions from that.
 	 */
 	if (rte->lateral)
@@ -1602,9 +1542,10 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte,
  *		Pull up a single simple VALUES RTE.
  *
  * jtnode is a RangeTblRef that has been identified as a simple VALUES RTE
- * by pull_up_subqueries.  We always return NULL indicating that the RTE
- * can be deleted entirely (all failure cases should have been detected by
- * is_simple_values()).
+ * by pull_up_subqueries.  We always return a RangeTblRef representing a
+ * RESULT RTE to replace it (all failure cases should have been detected by
+ * is_simple_values()).  Actually, what we return is just jtnode, because
+ * we replace the VALUES RTE in the rangetable with the RESULT RTE.
  *
  * rte is the RangeTblEntry referenced by jtnode.  Because of the limited
  * possible usage of VALUES RTEs, we do not need the remaining parameters
@@ -1703,11 +1644,23 @@ pull_up_simple_values(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte)
 	Assert(root->placeholder_list == NIL);
 
 	/*
-	 * Return NULL to signal deletion of the VALUES RTE from the parent
-	 * jointree (and set hasDeletedRTEs to ensure cleanup later).
+	 * Replace the VALUES RTE with a RESULT RTE.  The VALUES RTE is the only
+	 * rtable entry in the current query level, so this is easy.
 	 */
-	root->hasDeletedRTEs = true;
-	return NULL;
+	Assert(list_length(parse->rtable) == 1);
+
+	/* Create suitable RTE */
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RESULT;
+	rte->eref = makeAlias("*RESULT*", NIL);
+
+	/* Replace rangetable */
+	parse->rtable = list_make1(rte);
+
+	/* We could manufacture a new RangeTblRef, but the one we have is fine */
+	Assert(varno == 1);
+
+	return jtnode;
 }
 
 /*
@@ -1716,24 +1669,16 @@ pull_up_simple_values(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte)
  *	  to pull up into the parent query.
  *
  * rte is the RTE_VALUES RangeTblEntry to check.
- * deletion_ok is true if it'd be okay to delete the VALUES RTE entirely.
  */
 static bool
-is_simple_values(PlannerInfo *root, RangeTblEntry *rte, bool deletion_ok)
+is_simple_values(PlannerInfo *root, RangeTblEntry *rte)
 {
 	Assert(rte->rtekind == RTE_VALUES);
 
 	/*
-	 * We can only pull up a VALUES RTE if deletion_ok is true.  It's
-	 * basically the same case as a sub-select with empty FROM list; see
-	 * comments in is_simple_subquery().
-	 */
-	if (!deletion_ok)
-		return false;
-
-	/*
-	 * Also, there must be exactly one VALUES list, else it's not semantically
-	 * correct to delete the VALUES RTE.
+	 * There must be exactly one VALUES list, else it's not semantically
+	 * correct to replace the VALUES RTE with a RESULT RTE, nor would we have
+	 * a unique set of expressions to substitute into the parent query.
 	 */
 	if (list_length(rte->values_lists) != 1)
 		return false;
@@ -1746,8 +1691,8 @@ is_simple_values(PlannerInfo *root, RangeTblEntry *rte, bool deletion_ok)
 
 	/*
 	 * Don't pull up a VALUES that contains any set-returning or volatile
-	 * functions.  Again, the considerations here are basically identical to
-	 * restrictions on a subquery's targetlist.
+	 * functions.  The considerations here are basically identical to the
+	 * restrictions on a pull-able subquery's targetlist.
 	 */
 	if (expression_returns_set((Node *) rte->values_lists) ||
 		contain_volatile_functions((Node *) rte->values_lists))
@@ -1850,7 +1795,9 @@ is_safe_append_member(Query *subquery)
 	/*
 	 * It's only safe to pull up the child if its jointree contains exactly
 	 * one RTE, else the AppendRelInfo data structure breaks. The one base RTE
-	 * could be buried in several levels of FromExpr, however.
+	 * could be buried in several levels of FromExpr, however.  Also, if the
+	 * child's jointree is completely empty, we can pull up because
+	 * pull_up_simple_subquery will insert a single RTE_RESULT RTE instead.
 	 *
 	 * Also, the child can't have any WHERE quals because there's no place to
 	 * put them in an appendrel.  (This is a bit annoying...) If we didn't
@@ -1859,6 +1806,11 @@ is_safe_append_member(Query *subquery)
 	 * fix_append_rel_relids().
 	 */
 	jtnode = subquery->jointree;
+	Assert(IsA(jtnode, FromExpr));
+	/* Check the completely-empty case */
+	if (jtnode->fromlist == NIL && jtnode->quals == NULL)
+		return true;
+	/* Check the more general case */
 	while (IsA(jtnode, FromExpr))
 	{
 		if (jtnode->quals != NULL)
@@ -2014,6 +1966,7 @@ replace_vars_in_jointree(Node *jtnode,
 					case RTE_JOIN:
 					case RTE_CTE:
 					case RTE_NAMEDTUPLESTORE:
+					case RTE_RESULT:
 						/* these shouldn't be marked LATERAL */
 						Assert(false);
 						break;
@@ -2290,65 +2243,6 @@ pullup_replace_vars_subquery(Query *query,
 										   NULL);
 }
 
-/*
- * pull_up_subqueries_cleanup
- *		Recursively fix up jointree after deletion of some subqueries.
- *
- * The jointree now contains some NULL subtrees, which we need to get rid of.
- * In a FromExpr, just rebuild the child-node list with null entries deleted.
- * In an inner JOIN, replace the JoinExpr node with a one-child FromExpr.
- */
-static Node *
-pull_up_subqueries_cleanup(Node *jtnode)
-{
-	Assert(jtnode != NULL);
-	if (IsA(jtnode, RangeTblRef))
-	{
-		/* Nothing to do at leaf nodes. */
-	}
-	else if (IsA(jtnode, FromExpr))
-	{
-		FromExpr   *f = (FromExpr *) jtnode;
-		List	   *newfrom = NIL;
-		ListCell   *l;
-
-		foreach(l, f->fromlist)
-		{
-			Node	   *child = (Node *) lfirst(l);
-
-			if (child == NULL)
-				continue;
-			child = pull_up_subqueries_cleanup(child);
-			newfrom = lappend(newfrom, child);
-		}
-		f->fromlist = newfrom;
-	}
-	else if (IsA(jtnode, JoinExpr))
-	{
-		JoinExpr   *j = (JoinExpr *) jtnode;
-
-		if (j->larg)
-			j->larg = pull_up_subqueries_cleanup(j->larg);
-		if (j->rarg)
-			j->rarg = pull_up_subqueries_cleanup(j->rarg);
-		if (j->larg == NULL)
-		{
-			Assert(j->jointype == JOIN_INNER);
-			Assert(j->rarg != NULL);
-			return (Node *) makeFromExpr(list_make1(j->rarg), j->quals);
-		}
-		else if (j->rarg == NULL)
-		{
-			Assert(j->jointype == JOIN_INNER);
-			return (Node *) makeFromExpr(list_make1(j->larg), j->quals);
-		}
-	}
-	else
-		elog(ERROR, "unrecognized node type: %d",
-			 (int) nodeTag(jtnode));
-	return jtnode;
-}
-
 
 /*
  * flatten_simple_union_all
@@ -2858,9 +2752,399 @@ reduce_outer_joins_pass2(Node *jtnode,
 			 (int) nodeTag(jtnode));
 }
 
+
+/*
+ * remove_useless_result_rtes
+ *		Attempt to remove RTE_RESULT RTEs from the join tree.
+ *
+ * We can remove RTE_RESULT entries from the join tree using the knowledge
+ * that RTE_RESULT returns exactly one row and has no output columns.  Hence,
+ * if one is inner-joined to anything else, we can delete it.  Optimizations
+ * are also possible for some outer-join cases, as detailed below.
+ *
+ * Some of these optimizations depend on recognizing empty (constant-true)
+ * quals for FromExprs and JoinExprs.  That makes it useful to apply this
+ * optimization pass after expression preprocessing, since that will have
+ * eliminated constant-true quals, allowing more cases to be recognized as
+ * optimizable.  What's more, the usual reason for an RTE_RESULT to be present
+ * is that we pulled up a subquery or VALUES clause, thus very possibly
+ * replacing Vars with constants, making it more likely that a qual can be
+ * reduced to constant true.  Also, because some optimizations depend on
+ * the outer-join type, it's best to have done reduce_outer_joins() first.
+ *
+ * A PlaceHolderVar referencing an RTE_RESULT RTE poses an obstacle to this
+ * process: we must remove the RTE_RESULT's relid from the PHV's phrels, but
+ * we must not reduce the phrels set to empty.  If that would happen, and
+ * the RTE_RESULT is an immediate child of an outer join, we have to give up
+ * and not remove the RTE_RESULT: there is noplace else to evaluate the
+ * PlaceHolderVar.  (That is, in such cases the RTE_RESULT *does* have output
+ * columns.)  But if the RTE_RESULT is an immediate child of an inner join,
+ * we can change the PlaceHolderVar's phrels so as to evaluate it at the
+ * inner join instead.  This is OK because we really only care that PHVs are
+ * evaluated above or below the correct outer joins.
+ *
+ * We used to try to do this work as part of pull_up_subqueries() where the
+ * potentially-optimizable cases get introduced; but it's way simpler, and
+ * more effective, to do it separately.
+ */
+void
+remove_useless_result_rtes(PlannerInfo *root)
+{
+	ListCell   *cell;
+	ListCell   *prev;
+	ListCell   *next;
+
+	/* Top level of jointree must always be a FromExpr */
+	Assert(IsA(root->parse->jointree, FromExpr));
+	/* Recurse ... */
+	root->parse->jointree = (FromExpr *)
+		remove_useless_results_recurse(root, (Node *) root->parse->jointree);
+	/* We should still have a FromExpr */
+	Assert(IsA(root->parse->jointree, FromExpr));
+
+	/*
+	 * Remove any PlanRowMark referencing an RTE_RESULT RTE.  We obviously
+	 * must do that for any RTE_RESULT that we just removed.  But one for a
+	 * RTE that we did not remove can be dropped anyway: since the RTE has
+	 * only one possible output row, there is no need for EPQ to mark and
+	 * restore that row.
+	 *
+	 * It's necessary, not optional, to remove the PlanRowMark for a surviving
+	 * RTE_RESULT RTE; otherwise we'll generate a whole-row Var for the
+	 * RTE_RESULT, which the executor has no support for.
+	 */
+	prev = NULL;
+	for (cell = list_head(root->rowMarks); cell; cell = next)
+	{
+		PlanRowMark *rc = (PlanRowMark *) lfirst(cell);
+
+		next = lnext(cell);
+		if (rt_fetch(rc->rti, root->parse->rtable)->rtekind == RTE_RESULT)
+			root->rowMarks = list_delete_cell(root->rowMarks, cell, prev);
+		else
+			prev = cell;
+	}
+}
+
+/*
+ * remove_useless_results_recurse
+ *		Recursive guts of remove_useless_result_rtes.
+ *
+ * This recursively processes the jointree and returns a modified jointree.
+ */
+static Node *
+remove_useless_results_recurse(PlannerInfo *root, Node *jtnode)
+{
+	Assert(jtnode != NULL);
+	if (IsA(jtnode, RangeTblRef))
+	{
+		/* Can't immediately do anything with a RangeTblRef */
+	}
+	else if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		Relids		result_relids = NULL;
+		ListCell   *cell;
+		ListCell   *prev;
+		ListCell   *next;
+
+		/*
+		 * We can drop RTE_RESULT rels from the fromlist so long as at least
+		 * one child remains, since joining to a one-row table changes
+		 * nothing.  The easiest way to mechanize this rule is to modify the
+		 * list in-place, using list_delete_cell.
+		 */
+		prev = NULL;
+		for (cell = list_head(f->fromlist); cell; cell = next)
+		{
+			Node	   *child = (Node *) lfirst(cell);
+			int			varno;
+
+			/* Recursively transform child ... */
+			child = remove_useless_results_recurse(root, child);
+			/* ... and stick it back into the tree */
+			lfirst(cell) = child;
+			next = lnext(cell);
+
+			/*
+			 * If it's an RTE_RESULT with at least one sibling, we can drop
+			 * it.  We don't yet know what the inner join's final relid set
+			 * will be, so postpone cleanup of PHVs etc till after this loop.
+			 */
+			if (list_length(f->fromlist) > 1 &&
+				(varno = get_result_relid(root, child)) != 0)
+			{
+				f->fromlist = list_delete_cell(f->fromlist, cell, prev);
+				result_relids = bms_add_member(result_relids, varno);
+			}
+			else
+				prev = cell;
+		}
+
+		/*
+		 * Clean up if we dropped any RTE_RESULT RTEs.  This is a bit
+		 * inefficient if there's more than one, but it seems better to
+		 * optimize the support code for the single-relid case.
+		 */
+		if (result_relids)
+		{
+			int			varno = -1;
+
+			while ((varno = bms_next_member(result_relids, varno)) >= 0)
+				remove_result_refs(root, varno, (Node *) f);
+		}
+
+		/*
+		 * If we're not at the top of the jointree, it's valid to simplify a
+		 * degenerate FromExpr into its single child.  (At the top, we must
+		 * keep the FromExpr since Query.jointree is required to point to a
+		 * FromExpr.)
+		 */
+		if (f != root->parse->jointree &&
+			f->quals == NULL &&
+			list_length(f->fromlist) == 1)
+			return (Node *) linitial(f->fromlist);
+	}
+	else if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+		int			varno;
+
+		/* First, recurse */
+		j->larg = remove_useless_results_recurse(root, j->larg);
+		j->rarg = remove_useless_results_recurse(root, j->rarg);
+
+		/* Apply join-type-specific optimization rules */
+		switch (j->jointype)
+		{
+			case JOIN_INNER:
+
+				/*
+				 * An inner join is equivalent to a FromExpr, so if either
+				 * side was simplified to an RTE_RESULT rel, we can replace
+				 * the join with a FromExpr with just the other side; and if
+				 * the qual is empty (JOIN ON TRUE) then we can omit the
+				 * FromExpr as well.
+				 */
+				if ((varno = get_result_relid(root, j->larg)) != 0)
+				{
+					remove_result_refs(root, varno, j->rarg);
+					if (j->quals)
+						jtnode = (Node *)
+							makeFromExpr(list_make1(j->rarg), j->quals);
+					else
+						jtnode = j->rarg;
+				}
+				else if ((varno = get_result_relid(root, j->rarg)) != 0)
+				{
+					remove_result_refs(root, varno, j->larg);
+					if (j->quals)
+						jtnode = (Node *)
+							makeFromExpr(list_make1(j->larg), j->quals);
+					else
+						jtnode = j->larg;
+				}
+				break;
+			case JOIN_LEFT:
+
+				/*
+				 * We can simplify this case if the RHS is an RTE_RESULT, with
+				 * two different possibilities:
+				 *
+				 * If the qual is empty (JOIN ON TRUE), then the join can be
+				 * strength-reduced to a plain inner join, since each LHS row
+				 * necessarily has exactly one join partner.  So we can always
+				 * discard the RHS, much as in the JOIN_INNER case above.
+				 *
+				 * Otherwise, it's still true that each LHS row should be
+				 * returned exactly once, and since the RHS returns no columns
+				 * (unless there are PHVs that have to be evaluated there), we
+				 * don't much care if it's null-extended or not.  So in this
+				 * case also, we can just ignore the qual and discard the left
+				 * join.
+				 */
+				if ((varno = get_result_relid(root, j->rarg)) != 0 &&
+					(j->quals == NULL ||
+					 !find_dependent_phvs((Node *) root->parse, varno)))
+				{
+					remove_result_refs(root, varno, j->larg);
+					jtnode = j->larg;
+				}
+				break;
+			case JOIN_RIGHT:
+				/* Mirror-image of the JOIN_LEFT case */
+				if ((varno = get_result_relid(root, j->larg)) != 0 &&
+					(j->quals == NULL ||
+					 !find_dependent_phvs((Node *) root->parse, varno)))
+				{
+					remove_result_refs(root, varno, j->rarg);
+					jtnode = j->rarg;
+				}
+				break;
+			case JOIN_SEMI:
+
+				/*
+				 * We may simplify this case if the RHS is an RTE_RESULT; the
+				 * join qual becomes effectively just a filter qual for the
+				 * LHS, since we should either return the LHS row or not.  For
+				 * simplicity we inject the filter qual into a new FromExpr.
+				 *
+				 * Unlike the LEFT/RIGHT cases, we just Assert that there are
+				 * no PHVs that need to be evaluated at the semijoin's RHS,
+				 * since the rest of the query couldn't reference any outputs
+				 * of the semijoin's RHS.
+				 */
+				if ((varno = get_result_relid(root, j->rarg)) != 0)
+				{
+					Assert(!find_dependent_phvs((Node *) root->parse, varno));
+					remove_result_refs(root, varno, j->larg);
+					if (j->quals)
+						jtnode = (Node *)
+							makeFromExpr(list_make1(j->larg), j->quals);
+					else
+						jtnode = j->larg;
+				}
+				break;
+			case JOIN_FULL:
+			case JOIN_ANTI:
+				/* We have no special smarts for these cases */
+				break;
+			default:
+				elog(ERROR, "unrecognized join type: %d",
+					 (int) j->jointype);
+				break;
+		}
+	}
+	else
+		elog(ERROR, "unrecognized node type: %d",
+			 (int) nodeTag(jtnode));
+	return jtnode;
+}
+
+/*
+ * get_result_relid
+ *		If jtnode is a RangeTblRef for an RTE_RESULT RTE, return its relid;
+ *		otherwise return 0.
+ */
+static inline int
+get_result_relid(PlannerInfo *root, Node *jtnode)
+{
+	int			varno;
+
+	if (!IsA(jtnode, RangeTblRef))
+		return 0;
+	varno = ((RangeTblRef *) jtnode)->rtindex;
+	if (rt_fetch(varno, root->parse->rtable)->rtekind != RTE_RESULT)
+		return 0;
+	return varno;
+}
+
+/*
+ * remove_result_refs
+ *		Helper routine for dropping an unneeded RTE_RESULT RTE.
+ *
+ * This doesn't physically remove the RTE from the jointree, because that's
+ * more easily handled in remove_useless_results_recurse.  What it does do
+ * is the necessary cleanup in the rest of the tree: we must adjust any PHVs
+ * that may reference the RTE.  Be sure to call this at a point where the
+ * jointree is valid (no disconnected nodes).
+ *
+ * Note that we don't need to process the append_rel_list, since RTEs
+ * referenced directly in the jointree won't be appendrel members.
+ *
+ * varno is the RTE_RESULT's relid.
+ * newjtloc is the jointree location at which any PHVs referencing the
+ * RTE_RESULT should be evaluated instead.
+ */
+static void
+remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc)
+{
+	/* Fix up PlaceHolderVars as needed */
+	/* If there are no PHVs anywhere, we can skip this bit */
+	if (root->glob->lastPHId != 0)
+	{
+		Relids		subrelids;
+
+		subrelids = get_relids_in_jointree(newjtloc, false);
+		Assert(!bms_is_empty(subrelids));
+		substitute_phv_relids((Node *) root->parse, varno, subrelids);
+	}
+
+	/*
+	 * We also need to remove any PlanRowMark referencing the RTE, but we
+	 * postpone that work until we return to remove_useless_result_rtes.
+	 */
+}
+
+
+/*
+ * find_dependent_phvs - are there any PlaceHolderVars whose relids are
+ * exactly the given varno?
+ */
+
+typedef struct
+{
+	Relids		relids;
+	int			sublevels_up;
+} find_dependent_phvs_context;
+
+static bool
+find_dependent_phvs_walker(Node *node,
+						   find_dependent_phvs_context *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, PlaceHolderVar))
+	{
+		PlaceHolderVar *phv = (PlaceHolderVar *) node;
+
+		if (phv->phlevelsup == context->sublevels_up &&
+			bms_equal(context->relids, phv->phrels))
+			return true;
+		/* fall through to examine children */
+	}
+	if (IsA(node, Query))
+	{
+		/* Recurse into subselects */
+		bool		result;
+
+		context->sublevels_up++;
+		result = query_tree_walker((Query *) node,
+								   find_dependent_phvs_walker,
+								   (void *) context, 0);
+		context->sublevels_up--;
+		return result;
+	}
+	/* Shouldn't need to handle planner auxiliary nodes here */
+	Assert(!IsA(node, SpecialJoinInfo));
+	Assert(!IsA(node, AppendRelInfo));
+	Assert(!IsA(node, PlaceHolderInfo));
+	Assert(!IsA(node, MinMaxAggInfo));
+
+	return expression_tree_walker(node, find_dependent_phvs_walker,
+								  (void *) context);
+}
+
+static bool
+find_dependent_phvs(Node *node, int varno)
+{
+	find_dependent_phvs_context context;
+
+	context.relids = bms_make_singleton(varno);
+	context.sublevels_up = 0;
+
+	/*
+	 * Must be prepared to start with a Query or a bare expression tree.
+	 */
+	return query_or_expression_tree_walker(node,
+										   find_dependent_phvs_walker,
+										   (void *) &context,
+										   0);
+}
+
 /*
- * substitute_multiple_relids - adjust node relid sets after pulling up
- * a subquery
+ * substitute_phv_relids - adjust PlaceHolderVar relid sets after pulling up
+ * a subquery or removing an RTE_RESULT jointree item
  *
  * Find any PlaceHolderVar nodes in the given tree that reference the
  * pulled-up relid, and change them to reference the replacement relid(s).
@@ -2876,11 +3160,11 @@ typedef struct
 	int			varno;
 	int			sublevels_up;
 	Relids		subrelids;
-} substitute_multiple_relids_context;
+} substitute_phv_relids_context;
 
 static bool
-substitute_multiple_relids_walker(Node *node,
-								  substitute_multiple_relids_context *context)
+substitute_phv_relids_walker(Node *node,
+							 substitute_phv_relids_context *context)
 {
 	if (node == NULL)
 		return false;
@@ -2895,6 +3179,8 @@ substitute_multiple_relids_walker(Node *node,
 									context->subrelids);
 			phv->phrels = bms_del_member(phv->phrels,
 										 context->varno);
+			/* Assert we haven't broken the PHV */
+			Assert(!bms_is_empty(phv->phrels));
 		}
 		/* fall through to examine children */
 	}
@@ -2905,7 +3191,7 @@ substitute_multiple_relids_walker(Node *node,
 
 		context->sublevels_up++;
 		result = query_tree_walker((Query *) node,
-								   substitute_multiple_relids_walker,
+								   substitute_phv_relids_walker,
 								   (void *) context, 0);
 		context->sublevels_up--;
 		return result;
@@ -2916,14 +3202,14 @@ substitute_multiple_relids_walker(Node *node,
 	Assert(!IsA(node, PlaceHolderInfo));
 	Assert(!IsA(node, MinMaxAggInfo));
 
-	return expression_tree_walker(node, substitute_multiple_relids_walker,
+	return expression_tree_walker(node, substitute_phv_relids_walker,
 								  (void *) context);
 }
 
 static void
-substitute_multiple_relids(Node *node, int varno, Relids subrelids)
+substitute_phv_relids(Node *node, int varno, Relids subrelids)
 {
-	substitute_multiple_relids_context context;
+	substitute_phv_relids_context context;
 
 	context.varno = varno;
 	context.sublevels_up = 0;
@@ -2933,7 +3219,7 @@ substitute_multiple_relids(Node *node, int varno, Relids subrelids)
 	 * Must be prepared to start with a Query or a bare expression tree.
 	 */
 	query_or_expression_tree_walker(node,
-									substitute_multiple_relids_walker,
+									substitute_phv_relids_walker,
 									(void *) &context,
 									0);
 }
@@ -2943,7 +3229,7 @@ substitute_multiple_relids(Node *node, int varno, Relids subrelids)
  *
  * When we pull up a subquery, any AppendRelInfo references to the subquery's
  * RT index have to be replaced by the substituted relid (and there had better
- * be only one).  We also need to apply substitute_multiple_relids to their
+ * be only one).  We also need to apply substitute_phv_relids to their
  * translated_vars lists, since those might contain PlaceHolderVars.
  *
  * We assume we may modify the AppendRelInfo nodes in-place.
@@ -2974,9 +3260,9 @@ fix_append_rel_relids(List *append_rel_list, int varno, Relids subrelids)
 			appinfo->child_relid = subvarno;
 		}
 
-		/* Also finish fixups for its translated vars */
-		substitute_multiple_relids((Node *) appinfo->translated_vars,
-								   varno, subrelids);
+		/* Also fix up any PHVs in its translated vars */
+		substitute_phv_relids((Node *) appinfo->translated_vars,
+							  varno, subrelids);
 	}
 }
 
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f0ef102..49f581c 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1709,7 +1709,7 @@ contain_leaked_vars_walker(Node *node, void *context)
  * find_nonnullable_vars() is that the tested conditions really are different:
  * a clause like "t1.v1 IS NOT NULL OR t1.v2 IS NOT NULL" does not prove
  * that either v1 or v2 can't be NULL, but it does prove that the t1 row
- * as a whole can't be all-NULL.
+ * as a whole can't be all-NULL.  Also, the behavior for PHVs is different.
  *
  * top_level is true while scanning top-level AND/OR structure; here, showing
  * the result is either FALSE or NULL is good enough.  top_level is false when
@@ -1895,7 +1895,24 @@ find_nonnullable_rels_walker(Node *node, bool top_level)
 	{
 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
 
+		/*
+		 * If the contained expression forces any rels non-nullable, so does
+		 * the PHV.
+		 */
 		result = find_nonnullable_rels_walker((Node *) phv->phexpr, top_level);
+
+		/*
+		 * If the PHV's syntactic scope is exactly one rel, it will be forced
+		 * to be evaluated at that rel, and so it will behave like a Var of
+		 * that rel: if the rel's entire output goes to null, so will the PHV.
+		 * (If the syntactic scope is a join, we know that the PHV will go to
+		 * null if the whole join does; but that is AND semantics while we
+		 * need OR semantics for find_nonnullable_rels' result, so we can't do
+		 * anything with the knowledge.)
+		 */
+		if (phv->phlevelsup == 0 &&
+			bms_membership(phv->phrels) == BMS_SINGLETON)
+			result = bms_add_members(result, phv->phrels);
 	}
 	return result;
 }
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b2637d0..0402ffe 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1430,17 +1430,17 @@ create_merge_append_path(PlannerInfo *root,
 }
 
 /*
- * create_result_path
+ * create_group_result_path
  *	  Creates a path representing a Result-and-nothing-else plan.
  *
- * This is only used for degenerate cases, such as a query with an empty
- * jointree.
+ * This is only used for degenerate grouping cases, in which we know we
+ * need to produce one result row, possibly filtered by a HAVING qual.
  */
-ResultPath *
-create_result_path(PlannerInfo *root, RelOptInfo *rel,
-				   PathTarget *target, List *resconstantqual)
+GroupResultPath *
+create_group_result_path(PlannerInfo *root, RelOptInfo *rel,
+						 PathTarget *target, List *havingqual)
 {
-	ResultPath *pathnode = makeNode(ResultPath);
+	GroupResultPath *pathnode = makeNode(GroupResultPath);
 
 	pathnode->path.pathtype = T_Result;
 	pathnode->path.parent = rel;
@@ -1450,9 +1450,13 @@ create_result_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->path.parallel_safe = rel->consider_parallel;
 	pathnode->path.parallel_workers = 0;
 	pathnode->path.pathkeys = NIL;
-	pathnode->quals = resconstantqual;
+	pathnode->quals = havingqual;
 
-	/* Hardly worth defining a cost_result() function ... just do it */
+	/*
+	 * We can't quite use cost_resultscan() because the quals we want to
+	 * account for are not baserestrict quals of the rel.  Might as well just
+	 * hack it here.
+	 */
 	pathnode->path.rows = 1;
 	pathnode->path.startup_cost = target->cost.startup;
 	pathnode->path.total_cost = target->cost.startup +
@@ -1462,12 +1466,12 @@ create_result_path(PlannerInfo *root, RelOptInfo *rel,
 	 * Add cost of qual, if any --- but we ignore its selectivity, since our
 	 * rowcount estimate should be 1 no matter what the qual is.
 	 */
-	if (resconstantqual)
+	if (havingqual)
 	{
 		QualCost	qual_cost;
 
-		cost_qual_eval(&qual_cost, resconstantqual, root);
-		/* resconstantqual is evaluated once at startup */
+		cost_qual_eval(&qual_cost, havingqual, root);
+		/* havingqual is evaluated once at startup */
 		pathnode->path.startup_cost += qual_cost.startup + qual_cost.per_tuple;
 		pathnode->path.total_cost += qual_cost.startup + qual_cost.per_tuple;
 	}
@@ -2021,6 +2025,32 @@ create_namedtuplestorescan_path(PlannerInfo *root, RelOptInfo *rel,
 }
 
 /*
+ * create_resultscan_path
+ *	  Creates a path corresponding to a scan of an RTE_RESULT relation,
+ *	  returning the pathnode.
+ */
+Path *
+create_resultscan_path(PlannerInfo *root, RelOptInfo *rel,
+					   Relids required_outer)
+{
+	Path	   *pathnode = makeNode(Path);
+
+	pathnode->pathtype = T_Result;
+	pathnode->parent = rel;
+	pathnode->pathtarget = rel->reltarget;
+	pathnode->param_info = get_baserel_parampathinfo(root, rel,
+													 required_outer);
+	pathnode->parallel_aware = false;
+	pathnode->parallel_safe = rel->consider_parallel;
+	pathnode->parallel_workers = 0;
+	pathnode->pathkeys = NIL;	/* result is always unordered */
+
+	cost_resultscan(pathnode, root, rel, pathnode->param_info);
+
+	return pathnode;
+}
+
+/*
  * create_worktablescan_path
  *	  Creates a path corresponding to a scan of a self-reference CTE,
  *	  returning the pathnode.
@@ -3560,6 +3590,11 @@ reparameterize_path(PlannerInfo *root, Path *path,
 														 spath->path.pathkeys,
 														 required_outer);
 			}
+		case T_Result:
+			/* Supported only for RTE_RESULT scan paths */
+			if (IsA(path, Path))
+				return create_resultscan_path(root, rel, required_outer);
+			break;
 		case T_Append:
 			{
 				AppendPath *apath = (AppendPath *) path;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 48ffc5f..b555e30 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1628,6 +1628,7 @@ build_physical_tlist(PlannerInfo *root, RelOptInfo *rel)
 		case RTE_VALUES:
 		case RTE_CTE:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_RESULT:
 			/* Not all of these can have dropped cols, but share code anyway */
 			expandRTE(rte, varno, 0, -1, true /* include dropped */ ,
 					  NULL, &colvars);
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index fe83ec4..f04c6b7 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -247,6 +247,13 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 			rel->attr_widths = (int32 *)
 				palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32));
 			break;
+		case RTE_RESULT:
+			/* RTE_RESULT has no columns, nor could it have whole-row Var */
+			rel->min_attr = 0;
+			rel->max_attr = -1;
+			rel->attr_needed = NULL;
+			rel->attr_widths = NULL;
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d",
 				 (int) rte->rtekind);
@@ -1109,36 +1116,6 @@ subbuild_joinrel_joinlist(RelOptInfo *joinrel,
 
 
 /*
- * build_empty_join_rel
- *		Build a dummy join relation describing an empty set of base rels.
- *
- * This is used for queries with empty FROM clauses, such as "SELECT 2+2" or
- * "INSERT INTO foo VALUES(...)".  We don't try very hard to make the empty
- * joinrel completely valid, since no real planning will be done with it ---
- * we just need it to carry a simple Result path out of query_planner().
- */
-RelOptInfo *
-build_empty_join_rel(PlannerInfo *root)
-{
-	RelOptInfo *joinrel;
-
-	/* The dummy join relation should be the only one ... */
-	Assert(root->join_rel_list == NIL);
-
-	joinrel = makeNode(RelOptInfo);
-	joinrel->reloptkind = RELOPT_JOINREL;
-	joinrel->relids = NULL;		/* empty set */
-	joinrel->rows = 1;			/* we produce one row for such cases */
-	joinrel->rtekind = RTE_JOIN;
-	joinrel->reltarget = create_empty_pathtarget();
-
-	root->join_rel_list = lappend(root->join_rel_list, joinrel);
-
-	return joinrel;
-}
-
-
-/*
  * fetch_upper_rel
  *		Build a RelOptInfo describing some post-scan/join query processing,
  *		or return a pre-existing one if somebody already built it.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 5ff6964..81d1c7d 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -2878,6 +2878,9 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
 											LCS_asString(lc->strength)),
 									 parser_errposition(pstate, thisrel->location)));
 							break;
+
+							/* Shouldn't be possible to see RTE_RESULT here */
+
 						default:
 							elog(ERROR, "unrecognized RTE type: %d",
 								 (int) rte->rtekind);
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index dfbc1cc..4e2e584 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2517,6 +2517,9 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
 				}
 			}
 			break;
+		case RTE_RESULT:
+			/* These expose no columns, so nothing to do */
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind);
 	}
@@ -2909,6 +2912,14 @@ get_rte_attribute_type(RangeTblEntry *rte, AttrNumber attnum,
 									rte->eref->aliasname)));
 			}
 			break;
+		case RTE_RESULT:
+			/* this probably can't happen ... */
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column %d of relation \"%s\" does not exist",
+							attnum,
+							rte->eref->aliasname)));
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind);
 	}
@@ -3037,6 +3048,15 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum)
 				result = false; /* keep compiler quiet */
 			}
 			break;
+		case RTE_RESULT:
+			/* this probably can't happen ... */
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column %d of relation \"%s\" does not exist",
+							attnum,
+							rte->eref->aliasname)));
+			result = false;		/* keep compiler quiet */
+			break;
 		default:
 			elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind);
 			result = false;		/* keep compiler quiet */
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index ccd396b..561d877 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -398,6 +398,7 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle,
 		case RTE_VALUES:
 		case RTE_TABLEFUNC:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_RESULT:
 			/* not a simple relation, leave it unmarked */
 			break;
 		case RTE_CTE:
@@ -1531,6 +1532,7 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup)
 		case RTE_RELATION:
 		case RTE_VALUES:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_RESULT:
 
 			/*
 			 * This case should not occur: a column of a table, values list,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 77811f6..af6fa00 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7010,6 +7010,7 @@ get_name_for_var_field(Var *var, int fieldno,
 		case RTE_RELATION:
 		case RTE_VALUES:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_RESULT:
 
 			/*
 			 * This case should not occur: a column of a table, values list,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 10dac60..4808a9e 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -237,7 +237,7 @@ typedef enum NodeTag
 	T_HashPath,
 	T_AppendPath,
 	T_MergeAppendPath,
-	T_ResultPath,
+	T_GroupResultPath,
 	T_MaterialPath,
 	T_UniquePath,
 	T_GatherPath,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 27782fe..3ba9240 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -950,7 +950,10 @@ typedef enum RTEKind
 	RTE_TABLEFUNC,				/* TableFunc(.., column list) */
 	RTE_VALUES,					/* VALUES (<exprlist>), (<exprlist>), ... */
 	RTE_CTE,					/* common table expr (WITH list element) */
-	RTE_NAMEDTUPLESTORE			/* tuplestore, e.g. for AFTER triggers */
+	RTE_NAMEDTUPLESTORE,		/* tuplestore, e.g. for AFTER triggers */
+	RTE_RESULT					/* RTE represents an empty FROM clause; such
+								 * RTEs are added by the planner, they're not
+								 * present during parsing or rewriting */
 } RTEKind;
 
 typedef struct RangeTblEntry
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 3430061..420ca05 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -324,7 +324,6 @@ typedef struct PlannerInfo
 									 * partitioned table */
 	bool		hasJoinRTEs;	/* true if any RTEs are RTE_JOIN kind */
 	bool		hasLateralRTEs; /* true if any RTEs are marked LATERAL */
-	bool		hasDeletedRTEs; /* true if any RTE was deleted from jointree */
 	bool		hasHavingQual;	/* true if havingQual was non-null */
 	bool		hasPseudoConstantQuals; /* true if any RestrictInfo has
 										 * pseudoconstant = true */
@@ -1345,17 +1344,17 @@ typedef struct MergeAppendPath
 } MergeAppendPath;
 
 /*
- * ResultPath represents use of a Result plan node to compute a variable-free
- * targetlist with no underlying tables (a "SELECT expressions" query).
- * The query could have a WHERE clause, too, represented by "quals".
+ * GroupResultPath represents use of a Result plan node to compute the
+ * output of a degenerate GROUP BY case, wherein we know we should produce
+ * exactly one row, which might then be filtered by a HAVING qual.
  *
  * Note that quals is a list of bare clauses, not RestrictInfos.
  */
-typedef struct ResultPath
+typedef struct GroupResultPath
 {
 	Path		path;
 	List	   *quals;
-} ResultPath;
+} GroupResultPath;
 
 /*
  * MaterialPath represents use of a Material plan node, i.e., caching of
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index e7005b4..623f733 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -105,6 +105,8 @@ extern void cost_ctescan(Path *path, PlannerInfo *root,
 			 RelOptInfo *baserel, ParamPathInfo *param_info);
 extern void cost_namedtuplestorescan(Path *path, PlannerInfo *root,
 						 RelOptInfo *baserel, ParamPathInfo *param_info);
+extern void cost_resultscan(Path *path, PlannerInfo *root,
+				RelOptInfo *baserel, ParamPathInfo *param_info);
 extern void cost_recursive_union(Path *runion, Path *nrterm, Path *rterm);
 extern void cost_sort(Path *path, PlannerInfo *root,
 		  List *pathkeys, Cost input_cost, double tuples, int width,
@@ -196,6 +198,7 @@ extern void set_cte_size_estimates(PlannerInfo *root, RelOptInfo *rel,
 					   double cte_rows);
 extern void set_tablefunc_size_estimates(PlannerInfo *root, RelOptInfo *rel);
 extern void set_namedtuplestore_size_estimates(PlannerInfo *root, RelOptInfo *rel);
+extern void set_result_size_estimates(PlannerInfo *root, RelOptInfo *rel);
 extern void set_foreign_size_estimates(PlannerInfo *root, RelOptInfo *rel);
 extern PathTarget *set_pathtarget_cost_width(PlannerInfo *root, PathTarget *target);
 extern double compute_bitmap_pages(PlannerInfo *root, RelOptInfo *baserel,
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index bd905d3..aaaf3f4 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -75,8 +75,10 @@ extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 						 List *pathkeys,
 						 Relids required_outer,
 						 List *partitioned_rels);
-extern ResultPath *create_result_path(PlannerInfo *root, RelOptInfo *rel,
-				   PathTarget *target, List *resconstantqual);
+extern GroupResultPath *create_group_result_path(PlannerInfo *root,
+						 RelOptInfo *rel,
+						 PathTarget *target,
+						 List *havingqual);
 extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath);
 extern UniquePath *create_unique_path(PlannerInfo *root, RelOptInfo *rel,
 				   Path *subpath, SpecialJoinInfo *sjinfo);
@@ -105,6 +107,8 @@ extern Path *create_ctescan_path(PlannerInfo *root, RelOptInfo *rel,
 					Relids required_outer);
 extern Path *create_namedtuplestorescan_path(PlannerInfo *root, RelOptInfo *rel,
 								Relids required_outer);
+extern Path *create_resultscan_path(PlannerInfo *root, RelOptInfo *rel,
+					   Relids required_outer);
 extern Path *create_worktablescan_path(PlannerInfo *root, RelOptInfo *rel,
 						  Relids required_outer);
 extern ForeignPath *create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel,
@@ -275,7 +279,6 @@ extern Relids min_join_parameterization(PlannerInfo *root,
 						  Relids joinrelids,
 						  RelOptInfo *outer_rel,
 						  RelOptInfo *inner_rel);
-extern RelOptInfo *build_empty_join_rel(PlannerInfo *root);
 extern RelOptInfo *fetch_upper_rel(PlannerInfo *root, UpperRelationKind kind,
 				Relids relids);
 extern Relids find_childrel_parents(PlannerInfo *root, RelOptInfo *rel);
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index 62d45dd..a03a024 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -21,11 +21,13 @@
 /*
  * prototypes for prepjointree.c
  */
+extern void replace_empty_jointree(Query *parse);
 extern void pull_up_sublinks(PlannerInfo *root);
 extern void inline_set_returning_functions(PlannerInfo *root);
 extern void pull_up_subqueries(PlannerInfo *root);
 extern void flatten_simple_union_all(PlannerInfo *root);
 extern void reduce_outer_joins(PlannerInfo *root);
+extern void remove_useless_result_rtes(PlannerInfo *root);
 extern Relids get_relids_in_jointree(Node *jtnode, bool include_joins);
 extern Relids get_relids_for_join(PlannerInfo *root, int joinrelid);
 
diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out
index 49b3fb3..bbbb62e 100644
--- a/src/test/isolation/expected/eval-plan-qual.out
+++ b/src/test/isolation/expected/eval-plan-qual.out
@@ -239,9 +239,9 @@ id             value
 starting permutation: wrjt selectjoinforupdate c2 c1
 step wrjt: UPDATE jointest SET data = 42 WHERE id = 7;
 step selectjoinforupdate: 
-	set enable_nestloop to 0;
-	set enable_hashjoin to 0;
-	set enable_seqscan to 0;
+	set local enable_nestloop to 0;
+	set local enable_hashjoin to 0;
+	set local enable_seqscan to 0;
 	explain (costs off)
 	select * from jointest a join jointest b on a.id=b.id for update;
 	select * from jointest a join jointest b on a.id=b.id for update;
@@ -269,6 +269,45 @@ id             data           id             data
 10             0              10             0              
 step c1: COMMIT;
 
+starting permutation: wrjt selectresultforupdate c2 c1
+step wrjt: UPDATE jointest SET data = 42 WHERE id = 7;
+step selectresultforupdate: 
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y;
+	explain (verbose, costs off)
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y for update of jt, ss1, ss2;
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y for update of jt, ss1, ss2;
+ <waiting ...>
+step c2: COMMIT;
+step selectresultforupdate: <... completed>
+x              y              id             value          id             data           
+
+1              7              1              tableAValue    7              0              
+QUERY PLAN     
+
+LockRows       
+  Output: 1, 7, a.id, a.value, jt.id, jt.data, jt.ctid, a.ctid
+  ->  Nested Loop Left Join
+        Output: 1, 7, a.id, a.value, jt.id, jt.data, jt.ctid, a.ctid
+        ->  Nested Loop
+              Output: jt.id, jt.data, jt.ctid
+              ->  Seq Scan on public.jointest jt
+                    Output: jt.id, jt.data, jt.ctid
+                    Filter: (jt.id = 7)
+              ->  Result
+        ->  Seq Scan on public.table_a a
+              Output: a.id, a.value, a.ctid
+              Filter: (a.id = 1)
+x              y              id             value          id             data           
+
+1              7              1              tableAValue    7              42             
+step c1: COMMIT;
+
 starting permutation: wrtwcte multireadwcte c1 c2
 step wrtwcte: UPDATE table_a SET value = 'tableAValue2' WHERE id = 1;
 step multireadwcte: 
diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec
index 367922d..2e1b509 100644
--- a/src/test/isolation/specs/eval-plan-qual.spec
+++ b/src/test/isolation/specs/eval-plan-qual.spec
@@ -102,14 +102,29 @@ step "updateforcip"	{
 # these tests exercise mark/restore during EPQ recheck, cf bug #15032
 
 step "selectjoinforupdate"	{
-	set enable_nestloop to 0;
-	set enable_hashjoin to 0;
-	set enable_seqscan to 0;
+	set local enable_nestloop to 0;
+	set local enable_hashjoin to 0;
+	set local enable_seqscan to 0;
 	explain (costs off)
 	select * from jointest a join jointest b on a.id=b.id for update;
 	select * from jointest a join jointest b on a.id=b.id for update;
 }
 
+# these tests exercise Result plan nodes participating in EPQ
+
+step "selectresultforupdate"	{
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y;
+	explain (verbose, costs off)
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y for update of jt, ss1, ss2;
+	select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
+	  left join table_a a on a.id = x, jointest jt
+	  where jt.id = y for update of jt, ss1, ss2;
+}
+
 
 session "s2"
 setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
@@ -190,4 +205,5 @@ permutation "updateforcip" "updateforcip2" "c1" "c2" "read_a"
 permutation "updateforcip" "updateforcip3" "c1" "c2" "read_a"
 permutation "wrtwcte" "readwcte" "c1" "c2"
 permutation "wrjt" "selectjoinforupdate" "c2" "c1"
+permutation "wrjt" "selectresultforupdate" "c2" "c1"
 permutation "wrtwcte" "multireadwcte" "c1" "c2"
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 1f53780..4ed7559 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -31,6 +31,10 @@ INSERT INTO J2_TBL VALUES (5, -5);
 INSERT INTO J2_TBL VALUES (0, NULL);
 INSERT INTO J2_TBL VALUES (NULL, NULL);
 INSERT INTO J2_TBL VALUES (NULL, 0);
+-- useful in some tests below
+create temp table dual();
+insert into dual default values;
+analyze dual;
 --
 -- CORRELATION NAMES
 -- Make sure that table/column aliases are supported
@@ -2227,20 +2231,17 @@ explain (costs off)
 select * from int8_tbl i1 left join (int8_tbl i2 join
   (select 123 as x) ss on i2.q1 = x) on i1.q2 = i2.q2
 order by 1, 2;
-                   QUERY PLAN                    
--------------------------------------------------
+                QUERY PLAN                 
+-------------------------------------------
  Sort
    Sort Key: i1.q1, i1.q2
    ->  Hash Left Join
          Hash Cond: (i1.q2 = i2.q2)
          ->  Seq Scan on int8_tbl i1
          ->  Hash
-               ->  Hash Join
-                     Hash Cond: (i2.q1 = (123))
-                     ->  Seq Scan on int8_tbl i2
-                     ->  Hash
-                           ->  Result
-(11 rows)
+               ->  Seq Scan on int8_tbl i2
+                     Filter: (q1 = 123)
+(8 rows)
 
 select * from int8_tbl i1 left join (int8_tbl i2 join
   (select 123 as x) ss on i2.q1 = x) on i1.q2 = i2.q2
@@ -3133,8 +3134,8 @@ select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
   tenk1 t1
   inner join int4_tbl i1
     left join (select v1.x2, v2.y1, 11 AS d1
-               from (values(1,0)) v1(x1,x2)
-               left join (values(3,1)) v2(y1,y2)
+               from (select 1,0 from dual) v1(x1,x2)
+               left join (select 3,1 from dual) v2(y1,y2)
                on v1.x1 = v2.y2) subq1
     on (i1.f1 = subq1.x2)
   on (t1.unique2 = subq1.d1)
@@ -3144,27 +3145,26 @@ where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
                               QUERY PLAN                               
 -----------------------------------------------------------------------
  Nested Loop
-   Join Filter: (t1.stringu1 > t2.stringu2)
    ->  Nested Loop
-         Join Filter: ((0) = i1.f1)
+         Join Filter: (t1.stringu1 > t2.stringu2)
          ->  Nested Loop
                ->  Nested Loop
-                     Join Filter: ((1) = (1))
-                     ->  Result
-                     ->  Result
+                     ->  Seq Scan on dual
+                     ->  Seq Scan on dual dual_1
                ->  Index Scan using tenk1_unique2 on tenk1 t1
                      Index Cond: ((unique2 = (11)) AND (unique2 < 42))
-         ->  Seq Scan on int4_tbl i1
-   ->  Index Scan using tenk1_unique1 on tenk1 t2
-         Index Cond: (unique1 = (3))
-(14 rows)
+         ->  Index Scan using tenk1_unique1 on tenk1 t2
+               Index Cond: (unique1 = (3))
+   ->  Seq Scan on int4_tbl i1
+         Filter: (f1 = 0)
+(13 rows)
 
 select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
   tenk1 t1
   inner join int4_tbl i1
     left join (select v1.x2, v2.y1, 11 AS d1
-               from (values(1,0)) v1(x1,x2)
-               left join (values(3,1)) v2(y1,y2)
+               from (select 1,0 from dual) v1(x1,x2)
+               left join (select 3,1 from dual) v2(y1,y2)
                on v1.x1 = v2.y2) subq1
     on (i1.f1 = subq1.x2)
   on (t1.unique2 = subq1.d1)
@@ -3196,6 +3196,50 @@ where t1.unique1 < i4.f1;
 ----
 (0 rows)
 
+-- this variant is foldable by the remove-useless-RESULT-RTEs code
+explain (costs off)
+select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
+  tenk1 t1
+  inner join int4_tbl i1
+    left join (select v1.x2, v2.y1, 11 AS d1
+               from (values(1,0)) v1(x1,x2)
+               left join (values(3,1)) v2(y1,y2)
+               on v1.x1 = v2.y2) subq1
+    on (i1.f1 = subq1.x2)
+  on (t1.unique2 = subq1.d1)
+  left join tenk1 t2
+  on (subq1.y1 = t2.unique1)
+where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Nested Loop
+   Join Filter: (t1.stringu1 > t2.stringu2)
+   ->  Nested Loop
+         ->  Seq Scan on int4_tbl i1
+               Filter: (f1 = 0)
+         ->  Index Scan using tenk1_unique2 on tenk1 t1
+               Index Cond: ((unique2 = (11)) AND (unique2 < 42))
+   ->  Index Scan using tenk1_unique1 on tenk1 t2
+         Index Cond: (unique1 = (3))
+(9 rows)
+
+select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
+  tenk1 t1
+  inner join int4_tbl i1
+    left join (select v1.x2, v2.y1, 11 AS d1
+               from (values(1,0)) v1(x1,x2)
+               left join (values(3,1)) v2(y1,y2)
+               on v1.x1 = v2.y2) subq1
+    on (i1.f1 = subq1.x2)
+  on (t1.unique2 = subq1.d1)
+  left join tenk1 t2
+  on (subq1.y1 = t2.unique1)
+where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
+ unique2 | stringu1 | unique1 | stringu2 
+---------+----------+---------+----------
+      11 | WFAAAA   |       3 | LKIAAA
+(1 row)
+
 --
 -- test extraction of restriction OR clauses from join OR clause
 -- (we used to only do this for indexable clauses)
@@ -3596,7 +3640,7 @@ select t1.* from
                ->  Hash Right Join
                      Output: i8.q2
                      Hash Cond: ((NULL::integer) = i8b1.q2)
-                     ->  Hash Left Join
+                     ->  Hash Join
                            Output: i8.q2, (NULL::integer)
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
@@ -4018,10 +4062,10 @@ select * from
               QUERY PLAN               
 ---------------------------------------
  Nested Loop Left Join
-   Join Filter: ((1) = COALESCE((1)))
    ->  Result
    ->  Hash Full Join
          Hash Cond: (a1.unique1 = (1))
+         Filter: (1 = COALESCE((1)))
          ->  Seq Scan on tenk1 a1
          ->  Hash
                ->  Result
@@ -4951,9 +4995,6 @@ select v.* from
  -4567890123456789 |                  
 (20 rows)
 
-create temp table dual();
-insert into dual default values;
-analyze dual;
 select v.* from
   (int8_tbl x left join (select q1,(select coalesce(q2,0)) q2 from int8_tbl) y on x.q2 = y.q1)
   left join int4_tbl z on z.f1 = x.q2,
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 588d069..a54b4a5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -999,7 +999,7 @@ select * from
                         QUERY PLAN                        
 ----------------------------------------------------------
  Subquery Scan on ss
-   Output: x, u
+   Output: ss.x, ss.u
    Filter: tattle(ss.x, 8)
    ->  ProjectSet
          Output: 9, unnest('{1,2,3,11,12,13}'::integer[])
@@ -1061,7 +1061,7 @@ select * from
                         QUERY PLAN                        
 ----------------------------------------------------------
  Subquery Scan on ss
-   Output: x, u
+   Output: ss.x, ss.u
    Filter: tattle(ss.x, ss.u)
    ->  ProjectSet
          Output: 9, unnest('{1,2,3,11,12,13}'::integer[])
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 334a4dc..0aaa036 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -37,6 +37,12 @@ INSERT INTO J2_TBL VALUES (0, NULL);
 INSERT INTO J2_TBL VALUES (NULL, NULL);
 INSERT INTO J2_TBL VALUES (NULL, 0);
 
+-- useful in some tests below
+create temp table dual();
+insert into dual default values;
+analyze dual;
+
+
 --
 -- CORRELATION NAMES
 -- Make sure that table/column aliases are supported
@@ -940,8 +946,8 @@ select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
   tenk1 t1
   inner join int4_tbl i1
     left join (select v1.x2, v2.y1, 11 AS d1
-               from (values(1,0)) v1(x1,x2)
-               left join (values(3,1)) v2(y1,y2)
+               from (select 1,0 from dual) v1(x1,x2)
+               left join (select 3,1 from dual) v2(y1,y2)
                on v1.x1 = v2.y2) subq1
     on (i1.f1 = subq1.x2)
   on (t1.unique2 = subq1.d1)
@@ -953,8 +959,8 @@ select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
   tenk1 t1
   inner join int4_tbl i1
     left join (select v1.x2, v2.y1, 11 AS d1
-               from (values(1,0)) v1(x1,x2)
-               left join (values(3,1)) v2(y1,y2)
+               from (select 1,0 from dual) v1(x1,x2)
+               left join (select 3,1 from dual) v2(y1,y2)
                on v1.x1 = v2.y2) subq1
     on (i1.f1 = subq1.x2)
   on (t1.unique2 = subq1.d1)
@@ -980,6 +986,35 @@ select ss1.d1 from
   on t1.tenthous = ss1.d1
 where t1.unique1 < i4.f1;
 
+-- this variant is foldable by the remove-useless-RESULT-RTEs code
+
+explain (costs off)
+select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
+  tenk1 t1
+  inner join int4_tbl i1
+    left join (select v1.x2, v2.y1, 11 AS d1
+               from (values(1,0)) v1(x1,x2)
+               left join (values(3,1)) v2(y1,y2)
+               on v1.x1 = v2.y2) subq1
+    on (i1.f1 = subq1.x2)
+  on (t1.unique2 = subq1.d1)
+  left join tenk1 t2
+  on (subq1.y1 = t2.unique1)
+where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
+
+select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
+  tenk1 t1
+  inner join int4_tbl i1
+    left join (select v1.x2, v2.y1, 11 AS d1
+               from (values(1,0)) v1(x1,x2)
+               left join (values(3,1)) v2(y1,y2)
+               on v1.x1 = v2.y2) subq1
+    on (i1.f1 = subq1.x2)
+  on (t1.unique2 = subq1.d1)
+  left join tenk1 t2
+  on (subq1.y1 = t2.unique1)
+where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
+
 --
 -- test extraction of restriction OR clauses from join OR clause
 -- (we used to only do this for indexable clauses)
@@ -1661,9 +1696,6 @@ select v.* from
   (int8_tbl x left join (select q1,(select coalesce(q2,0)) q2 from int8_tbl) y on x.q2 = y.q1)
   left join int4_tbl z on z.f1 = x.q2,
   lateral (select x.q1,y.q1 union all select x.q2,y.q2) v(vx,vy);
-create temp table dual();
-insert into dual default values;
-analyze dual;
 select v.* from
   (int8_tbl x left join (select q1,(select coalesce(q2,0)) q2 from int8_tbl) y on x.q2 = y.q1)
   left join int4_tbl z on z.f1 = x.q2,
